Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

knifecycle

Package Overview
Dependencies
Maintainers
1
Versions
101
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

knifecycle - npm Package Compare versions

Comparing version 15.0.1 to 16.0.0

9

CHANGELOG.md

@@ -0,1 +1,10 @@

# [16.0.0](https://github.com/nfroidure/knifecycle/compare/v15.0.1...v16.0.0) (2023-08-16)
### Bug Fixes
* **build:** fix deps lock file ([ab9114e](https://github.com/nfroidure/knifecycle/commit/ab9114e2f6d89cffbcf442c1a0199d920a3dc438))
## [15.0.1](https://github.com/nfroidure/knifecycle/compare/v15.0.0...v15.0.1) (2023-05-28)

@@ -2,0 +11,0 @@

3

dist/build.js

@@ -86,4 +86,3 @@ import { SPECIAL_PROPS, parseDependencyDeclaration, initializer, } from './util.js';

.map((name) => {
if ('constant' ===
dependenciesHash[name].__initializer[SPECIAL_PROPS.TYPE]) {
if ('constant' === dependenciesHash[name].__initializer[SPECIAL_PROPS.TYPE]) {
return `

@@ -90,0 +89,0 @@ ${name}: Promise.resolve(${name}),`;

@@ -0,1 +1,2 @@

/* eslint-disable @typescript-eslint/no-explicit-any */
import { describe, test } from '@jest/globals';

@@ -117,3 +118,83 @@ import assert from 'assert';

});
// TODO: allow building with internal dependencies
test.skip('should work with simple internal services dependencies', async () => {
const $ = new Knifecycle();
$.register(constant('PWD', '~/my-project'));
$.register(initAutoloader);
$.register(initInitializerBuilder);
$.register(constant('$fatalError', {}));
const { buildInitializer } = await $.run(['buildInitializer']);
const content = await buildInitializer([
'dep1',
'finalMappedDep>dep3',
'$fatalError',
'$dispose',
'$siloContext',
]);
assert.equal(content, `
// Definition batch #0
import initDep1 from './services/dep1';
const NODE_ENV = "development";
// Definition batch #1
import initDep2 from './services/dep2';
// Definition batch #2
import initDep3 from './services/dep3';
export async function initialize(services = {}) {
// Initialization batch #0
const batch0 = {
dep1: initDep1({
}),
NODE_ENV: Promise.resolve(NODE_ENV),
};
await Promise.all(
Object.keys(batch0)
.map(key => batch0[key])
);
services['dep1'] = await batch0['dep1'];
services['NODE_ENV'] = await batch0['NODE_ENV'];
// Initialization batch #1
const batch1 = {
dep2: initDep2({
dep1: services['dep1'],
NODE_ENV: services['NODE_ENV'],
}).then(provider => provider.service),
};
await Promise.all(
Object.keys(batch1)
.map(key => batch1[key])
);
services['dep2'] = await batch1['dep2'];
// Initialization batch #2
const batch2 = {
dep3: initDep3({
dep2: services['dep2'],
dep1: services['dep1'],
depOpt: services['depOpt'],
}),
};
await Promise.all(
Object.keys(batch2)
.map(key => batch2[key])
);
services['dep3'] = await batch2['dep3'];
return {
dep1: services['dep1'],
finalMappedDep: services['dep3'],
};
}
`);
});
});
//# sourceMappingURL=build.test.js.map

@@ -6,2 +6,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';

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, };
export declare const RUN_DEPENDENT_NAME = "__run__";
export declare const SYSTEM_DEPENDENT_NAME = "__system__";
export declare const AUTOLOAD_DEPENDENT_NAME = "__autoloader__";
export declare const INJECTOR_DEPENDENT_NAME = "__injector__";
export declare const NO_PROVIDER: unique symbol;
export type KnifecycleOptions = {
sequential?: boolean;
};
export interface Injector<T extends Record<string, unknown>> {

@@ -16,9 +24,37 @@ (dependencies: DependencyDeclaration[]): Promise<T>;

}
export interface SiloContext<S> {
name: string;
servicesDescriptors: Map<DependencyDeclaration, Promise<Provider<S>>>;
servicesSequence: DependencyDeclaration[][];
servicesShutdownsPromises: Map<DependencyDeclaration, Promise<void>>;
export type SiloIndex = string;
export type BaseInitializerStateDescriptor<S, D extends Dependencies> = {
dependents: {
silo?: SiloIndex;
name: ServiceName;
optional: boolean;
}[];
initializerLoadPromise?: Promise<Initializer<S, D>>;
initializer?: Initializer<S, D>;
autoloaded: boolean;
};
export type SiloedInitializerStateDescriptor<S, D extends Dependencies> = BaseInitializerStateDescriptor<S, D> & {
silosInstances: Record<SiloIndex, {
dependency?: ServiceName;
provider?: NonNullable<Provider<S> | typeof NO_PROVIDER>;
providerLoadPromise?: Promise<void>;
instanceDisposePromise?: Promise<S>;
}>;
};
export type SingletonInitializerStateDescriptor<S, D extends Dependencies> = BaseInitializerStateDescriptor<S, D> & {
singletonProvider?: NonNullable<Provider<S> | typeof NO_PROVIDER>;
singletonProviderLoadPromise?: Promise<void>;
disposer?: Disposer;
fatalErrorPromise?: FatalErrorPromise;
};
export type AutoloadedInitializerStateDescriptor<S, D extends Dependencies> = BaseInitializerStateDescriptor<S, D> & {
autoloaded: true;
};
export type InitializerStateDescriptor<S, D extends Dependencies> = SingletonInitializerStateDescriptor<S, D> | SiloedInitializerStateDescriptor<S, D> | AutoloadedInitializerStateDescriptor<S, D>;
export interface SiloContext {
index: SiloIndex;
loadingServices: ServiceName[];
loadingSequences: ServiceName[][];
errorsPromises: Promise<void>[];
shutdownPromise?: Promise<void>;
_shutdownPromise?: Promise<void>;
throwFatalError?: (err: Error) => void;

@@ -34,16 +70,17 @@ }

$instance: Knifecycle;
$siloContext: SiloContext<unknown>;
$siloContext: SiloContext;
$fatalError: FatalErrorService;
};
declare class Knifecycle {
private _options;
private _silosCounter;
private _silosContexts;
private _initializers;
private _initializerResolvers;
private _singletonsServicesHandles;
private _singletonsServicesDescriptors;
private _singletonsServicesShutdownsPromises;
private shutdownPromise?;
private _initializersStates;
private _shutdownPromise?;
/**
* Create a new Knifecycle instance
* @param {Object} options
* An object with options
* @param {boolean} options.sequential
* Allows to load dependencies sequentially (usefull for debugging)
* @return {Knifecycle}

@@ -57,3 +94,3 @@ * The Knifecycle instance

*/
constructor();
constructor(options?: KnifecycleOptions);
/**

@@ -67,2 +104,5 @@ * Register an initializer

register<T extends Initializer<unknown, any>>(initializer: T): Knifecycle;
_checkInitializerOverride(serviceName: ServiceName): void;
_buildInitializerState(initializerState: InitializerStateDescriptor<any, any>, initializer: Initializer<unknown, any>): void;
_checkInitializerDependencies(initializer: Initializer<any, any>): void;
_lookupCircularDependencies(rootServiceName: ServiceName, dependencyDeclaration: DependencyDeclaration, declarationsStacks?: DependencyDeclaration[]): void;

@@ -123,2 +163,9 @@ /**

run<ID extends Record<string, unknown>>(dependenciesDeclarations: DependencyDeclaration[]): Promise<ID>;
_getInitializer(serviceName: ServiceName): Initializer<unknown, Dependencies> | undefined;
_getServiceProvider(siloContext: SiloContext, serviceName: ServiceName): Provider<unknown> | typeof NO_PROVIDER | undefined;
_loadInitializerDependencies(siloContext: SiloContext, parentsNames: ServiceName[], dependenciesDeclarations: DependencyDeclaration[], additionalDeclarations: DependencyDeclaration[]): Promise<Dependencies>;
_loadProvider(siloContext: SiloContext, serviceName: ServiceName, parentsNames: ServiceName[]): Promise<void>;
_getAutoloader(siloContext: SiloContext, parentsNames: ServiceName[]): Promise<Autoloader<Initializer<unknown, Dependencies<unknown>>> | undefined>;
_loadInitializer(siloContext: SiloContext, serviceName: ServiceName, parentsNames: ServiceName[]): Promise<void>;
_resolveDependencies(siloContext: SiloContext, loadingServices: ServiceName[], parentsNames: ServiceName[]): Promise<void>;
/**

@@ -144,68 +191,2 @@ * Destroy the Knifecycle instance

destroy(): Promise<void>;
/**
* Initialize or return a service descriptor
* @param {Object} siloContext
* Current execution silo context
* @param {String} serviceName
* Service name.
* @param {Object} options
* Options for service retrieval
* @param {Boolean} options.injectorContext
* Flag indicating the injection were initiated by the $injector
* @param {Boolean} options.autoloading
* Flag to indicating $autoload dependencies on the fly loading
* @param {String} serviceProvider
* Service provider.
* @return {Promise}
* Service descriptor promise.
*/
_getServiceDescriptor(siloContext: SiloContext<unknown>, serviceName: ServiceName, { injectorContext, autoloading, }: {
injectorContext: boolean;
autoloading: boolean;
}): Promise<Provider<unknown>>;
_findInitializer(siloContext: SiloContext<unknown>, serviceName: ServiceName, { injectorContext, autoloading, }: {
injectorContext: boolean;
autoloading: boolean;
}): Promise<ProviderInitializer<Record<string, unknown>, unknown>>;
_pickupSingletonServiceDescriptorPromise(serviceName: ServiceName): Promise<Provider<unknown>> | void;
/**
* Initialize a service descriptor
* @param {Object} siloContext
* Current execution silo context
* @param {String} serviceName
* Service name.
* @param {Object} options
* Options for service retrieval
* @param {Boolean} options.injectorContext
* Flag indicating the injection were initiated by the $injector
* @param {Boolean} options.autoloading
* Flag to indicating $autoload dependendencies on the fly loading.
* @return {Promise}
* Service dependencies hash promise.
*/
_initializeServiceDescriptor(siloContext: SiloContext<unknown>, serviceName: ServiceName, initializer: ProviderInitializer<Record<string, unknown>, unknown>, { autoloading, injectorContext, }: {
autoloading: boolean;
injectorContext: boolean;
}): Promise<Provider<unknown>>;
/**
* Initialize a service dependencies
* @param {Object} siloContext
* Current execution silo siloContext
* @param {String} serviceName
* Service name.
* @param {String} servicesDeclarations
* Dependencies declarations.
* @param {Object} options
* Options for service retrieval
* @param {Boolean} options.injectorContext
* Flag indicating the injection were initiated by the $injector
* @param {Boolean} options.autoloading
* Flag to indicating $autoload dependendencies on the fly loading.
* @return {Promise}
* Service dependencies hash promise.
*/
_initializeDependencies(siloContext: SiloContext<unknown>, serviceName: ServiceName, servicesDeclarations: DependencyDeclaration[], { injectorContext, autoloading, }: {
autoloading: boolean;
injectorContext: boolean;
}): Promise<Dependencies>;
}

@@ -212,0 +193,0 @@ 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, };

@@ -0,1 +1,2 @@

/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint max-len: ["warn", { "ignoreComments": true }] @typescript-eslint/no-this-alias: "warn" */

@@ -6,2 +7,7 @@ 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 initDebug from 'debug';
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');
const debug = initDebug('knifecycle');

@@ -14,13 +20,2 @@ const DISPOSE = '$dispose';

const FATAL_ERROR = '$fatalError';
const E_BAD_AUTOLOADED_INITIALIZER = 'E_BAD_AUTOLOADED_INITIALIZER';
const E_AUTOLOADED_INITIALIZER_MISMATCH = 'E_AUTOLOADED_INITIALIZER_MISMATCH';
const E_UNMATCHED_DEPENDENCY = 'E_UNMATCHED_DEPENDENCY';
const E_CIRCULAR_DEPENDENCY = 'E_CIRCULAR_DEPENDENCY';
const E_BAD_SERVICE_PROVIDER = 'E_BAD_SERVICE_PROVIDER';
const E_BAD_SERVICE_PROMISE = 'E_BAD_SERVICE_PROMISE';
const E_INSTANCE_DESTROYED = 'E_INSTANCE_DESTROYED';
const E_AUTOLOADER_DYNAMIC_DEPENDENCY = 'E_AUTOLOADER_DYNAMIC_DEPENDENCY';
const E_BAD_CLASS = 'E_BAD_CLASS';
const E_UNDEFINED_CONSTANT_INITIALIZER = 'E_UNDEFINED_CONSTANT_INITIALIZER';
const E_BAD_VALUED_NON_CONSTANT_INITIALIZER = 'E_BAD_VALUED_NON_CONSTANT_INITIALIZER';
/* Architecture Note #1: Knifecycle

@@ -42,2 +37,5 @@

set as a property.
In fact, the Knifecycle API is aimed to allow to statically
build its services load/unload code once in production.
*/

@@ -55,12 +53,13 @@ /* Architecture Note #1.1: OOP

class Knifecycle {
_options;
_silosCounter;
_silosContexts;
_initializers;
_initializerResolvers;
_singletonsServicesHandles;
_singletonsServicesDescriptors;
_singletonsServicesShutdownsPromises;
shutdownPromise;
_initializersStates;
_shutdownPromise;
/**
* Create a new Knifecycle instance
* @param {Object} options
* An object with options
* @param {boolean} options.sequential
* Allows to load dependencies sequentially (usefull for debugging)
* @return {Knifecycle}

@@ -74,14 +73,38 @@ * The Knifecycle instance

*/
constructor() {
constructor(options) {
this._options = options || {};
this._silosCounter = 0;
this._silosContexts = new Set();
this._initializers = new Map();
this._initializerResolvers = new Map();
this._singletonsServicesHandles = new Map();
this._singletonsServicesDescriptors = new Map();
this._singletonsServicesShutdownsPromises = new Map();
this._silosContexts = {};
this._initializersStates = {
[FATAL_ERROR]: {
initializer: service(async () => {
throw new YError('E_UNEXPECTED_INIT', FATAL_ERROR);
}, FATAL_ERROR),
autoloaded: false,
dependents: [],
silosInstances: {},
},
[SILO_CONTEXT]: {
initializer: service(async () => {
throw new YError('E_UNEXPECTED_INIT', SILO_CONTEXT);
}, SILO_CONTEXT),
autoloaded: false,
dependents: [],
silosInstances: {},
},
[DISPOSE]: {
initializer: service(async () => {
throw new YError('E_UNEXPECTED_INIT', DISPOSE);
}, DISPOSE),
autoloaded: false,
dependents: [],
silosInstances: {},
},
};
this.register(constant(INSTANCE, this));
const initInjectorProvider = provider(async ({ $siloContext }) => ({
service: async (dependenciesDeclarations) => _buildFinalHash(await this._initializeDependencies($siloContext, $siloContext.name, dependenciesDeclarations, { injectorContext: true, autoloading: false }), dependenciesDeclarations),
}), INJECTOR, [SILO_CONTEXT],
const initInjectorProvider = provider(async ({ $siloContext, $instance, }) => ({
service: async (dependenciesDeclarations) => {
return $instance._loadInitializerDependencies($siloContext, [INJECTOR_DEPENDENT_NAME], dependenciesDeclarations, []);
},
}), INJECTOR, [SILO_CONTEXT, INSTANCE],
// Despite its global definition, the injector

@@ -101,3 +124,3 @@ // depends on the silo context and then needs

- constants: a `constant` initializer resolves to
a constant value.
any constant value.
- services: a `service` initializer directly

@@ -111,8 +134,9 @@ resolve to the actual service it builds. It can

`fatalErrorPromise` that will be rejected if an
unrecoverable error happens.
unrecoverable error happens allowing Knifecycle
to terminate.
Initializers can be declared as singletons. This means
that they will be instanciated once for all for each
executions silos using them (we will cover this
topic later on).
Initializers can be declared as singletons (constants are
of course only singletons). This means that they will be
instanciated once for all for each executions silos using
them (we will cover this topic later on).
*/

@@ -127,37 +151,88 @@ /**

register(initializer) {
if (this.shutdownPromise) {
throw new YError(E_INSTANCE_DESTROYED);
if (this._shutdownPromise) {
throw new YError('E_INSTANCE_DESTROYED');
}
const initializerState = {
initializer,
autoloaded: false,
dependents: [],
};
this._checkInitializerOverride(initializer[SPECIAL_PROPS.NAME]);
this._buildInitializerState(initializerState, initializer);
this._initializersStates[initializer[SPECIAL_PROPS.NAME]] =
initializerState;
debug(`Registered an initializer: ${initializer[SPECIAL_PROPS.NAME]}`);
return this;
}
_checkInitializerOverride(serviceName) {
if (this._initializersStates[serviceName]) {
if ('initializer' in this._initializersStates[serviceName]) {
if (this._initializersStates[serviceName]?.dependents?.length) {
debug(`Override attempt of an already used initializer: ${serviceName}`);
throw new YError('E_INITIALIZER_ALREADY_INSTANCIATED', serviceName);
}
debug(`Overridden an initializer: ${serviceName}`);
}
}
}
_buildInitializerState(initializerState, initializer) {
unwrapInitializerProperties(initializer);
// Temporary cast constants into providers
// Best would be to threat each differently
// at dependencies initialization level to boost performances
if (initializer[SPECIAL_PROPS.TYPE] === 'constant') {
const value = initializer[SPECIAL_PROPS.VALUE];
if ('undefined' === typeof value) {
throw new YError(E_UNDEFINED_CONSTANT_INITIALIZER, initializer[SPECIAL_PROPS.NAME]);
const provider = {
service: initializer[SPECIAL_PROPS.VALUE],
};
initializerState.singletonProvider = provider;
initializerState.singletonProviderLoadPromise = Promise.resolve();
}
else {
this._checkInitializerDependencies(initializer);
if (!initializer[SPECIAL_PROPS.SINGLETON]) {
initializerState.silosInstances = {};
}
initializer = provider(async () => ({
service: value,
}), initializer[SPECIAL_PROPS.NAME], [], true);
// Needed for the build utils to still recognize
// this initializer as a constant value
initializer[SPECIAL_PROPS.VALUE] = value;
initializer[SPECIAL_PROPS.TYPE] = 'constant';
}
else if ('undefined' !== typeof initializer[SPECIAL_PROPS.VALUE]) {
throw new YError(E_BAD_VALUED_NON_CONSTANT_INITIALIZER, initializer[SPECIAL_PROPS.NAME]);
}
// Temporary cast service initializers into
// providers. Best would be to threat each differently
// at dependencies initialization level to boost performances
if ('service' === initializer[SPECIAL_PROPS.TYPE]) {
initializer = reuseSpecialProps(initializer, serviceAdapter.bind(null, initializer[SPECIAL_PROPS.NAME], initializer));
initializer[SPECIAL_PROPS.TYPE] = 'provider';
}
}
_checkInitializerDependencies(initializer) {
const initializerDependsOfItself = initializer[SPECIAL_PROPS.INJECT]
.map(_pickServiceNameFromDeclaration)
.map((dependencyDeclaration) => {
const serviceName = _pickServiceNameFromDeclaration(dependencyDeclaration);
if (
// TEMPFIX: let's build
initializer[SPECIAL_PROPS.NAME] !== 'BUILD_CONSTANTS' &&
// TEMPFIX: Those services are special...
![FATAL_ERROR, INJECTOR, SILO_CONTEXT].includes(serviceName) &&
initializer[SPECIAL_PROPS.SINGLETON] &&
this._initializersStates[serviceName] &&
'initializer' in this._initializersStates[serviceName] &&
this._initializersStates[serviceName]?.initializer &&
!this._initializersStates[serviceName]?.initializer?.[SPECIAL_PROPS.SINGLETON]) {
debug(`Found an inconsistent singleton initializer dependency: ${initializer[SPECIAL_PROPS.NAME]}`, serviceName, initializer);
throw new YError('E_BAD_SINGLETON_DEPENDENCIES', initializer[SPECIAL_PROPS.NAME], serviceName);
}
return serviceName;
})
.includes(initializer[SPECIAL_PROPS.NAME]);
if (
// TEMPFIX: let's build
initializer[SPECIAL_PROPS.NAME] !== 'BUILD_CONSTANTS' &&
!initializer[SPECIAL_PROPS.SINGLETON]) {
Object.keys(this._initializersStates)
.filter((serviceName) => ![
// TEMPFIX: Those services are special...
FATAL_ERROR,
INJECTOR,
SILO_CONTEXT,
].includes(serviceName))
.forEach((serviceName) => {
if (this._initializersStates[serviceName]?.initializer &&
this._initializersStates[serviceName]?.initializer?.[SPECIAL_PROPS.SINGLETON] &&
(this._initializersStates[serviceName]?.initializer?.[SPECIAL_PROPS.INJECT] || [])
.map(_pickServiceNameFromDeclaration)
.includes(initializer[SPECIAL_PROPS.NAME])) {
debug(`Found an inconsistent dependent initializer: ${initializer[SPECIAL_PROPS.NAME]}`, serviceName, initializer);
throw new YError('E_BAD_SINGLETON_DEPENDENCIES', serviceName, initializer[SPECIAL_PROPS.NAME]);
}
});
}
if (initializerDependsOfItself) {
throw new YError(E_CIRCULAR_DEPENDENCY, initializer[SPECIAL_PROPS.NAME]);
throw new YError('E_CIRCULAR_DEPENDENCY', initializer[SPECIAL_PROPS.NAME]);
}

@@ -167,42 +242,14 @@ initializer[SPECIAL_PROPS.INJECT].forEach((dependencyDeclaration) => {

});
if (this._initializers.has(initializer[SPECIAL_PROPS.NAME])) {
const initializedAsSingleton = this._singletonsServicesHandles.has(initializer[SPECIAL_PROPS.NAME]) &&
this._singletonsServicesDescriptors.has(initializer[SPECIAL_PROPS.NAME]) &&
!this._singletonsServicesDescriptors.get(initializer[SPECIAL_PROPS.NAME])?.preloaded;
const initializedAsInstance = [...this._silosContexts.values()].some((siloContext) => siloContext.servicesSequence.some((sequence) => sequence.includes(initializer[SPECIAL_PROPS.NAME])));
if (initializedAsSingleton || initializedAsInstance) {
throw new YError('E_INITIALIZER_ALREADY_INSTANCIATED', initializer[SPECIAL_PROPS.NAME]);
}
debug(`Overridden an initializer: ${initializer[SPECIAL_PROPS.NAME]}`);
}
else {
debug(`Registered an initializer: ${initializer[SPECIAL_PROPS.NAME]}`);
}
// Constants are singletons and constant so we can set it
// to singleton services descriptors map directly
if ('constant' === initializer[SPECIAL_PROPS.TYPE]) {
const handlesSet = new Set();
this._singletonsServicesHandles.set(initializer[SPECIAL_PROPS.NAME], handlesSet);
this._singletonsServicesDescriptors.set(initializer[SPECIAL_PROPS.NAME], {
preloaded: true,
// We do not directly use initializer[SPECIAL_PROPS.VALUE] here
// since it looks like there is a bug with Babel build that
// change functions to empty litteral objects
promise: initializer({}),
});
}
this._initializers.set(initializer[SPECIAL_PROPS.NAME], initializer);
return this;
}
_lookupCircularDependencies(rootServiceName, dependencyDeclaration, declarationsStacks = []) {
const serviceName = _pickServiceNameFromDeclaration(dependencyDeclaration);
const dependencyProvider = this._initializers.get(serviceName);
if (!dependencyProvider) {
const initializersState = this._initializersStates[serviceName];
if (!initializersState || !initializersState.initializer) {
return;
}
declarationsStacks = declarationsStacks.concat(dependencyDeclaration);
dependencyProvider[SPECIAL_PROPS.INJECT].forEach((childDependencyDeclaration) => {
(initializersState.initializer[SPECIAL_PROPS.INJECT] || []).forEach((childDependencyDeclaration) => {
const childServiceName = _pickServiceNameFromDeclaration(childDependencyDeclaration);
if (rootServiceName === childServiceName) {
throw new YError(E_CIRCULAR_DEPENDENCY, ...[rootServiceName]
throw new YError('E_CIRCULAR_DEPENDENCY', ...[rootServiceName]
.concat(declarationsStacks)

@@ -249,11 +296,13 @@ .concat(childDependencyDeclaration));

}) {
const servicesProviders = this._initializers;
const links = Array.from(servicesProviders.keys())
const initializersStates = this._initializersStates;
const links = Object.keys(initializersStates)
.filter((provider) => !provider.startsWith('$'))
.reduce((links, serviceName) => {
const serviceProvider = servicesProviders.get(serviceName);
if (!serviceProvider[SPECIAL_PROPS.INJECT].length) {
const initializerState = initializersStates[serviceName];
if (!initializerState ||
!initializerState.initializer ||
!initializerState.initializer[SPECIAL_PROPS.INJECT]?.length) {
return links;
}
return links.concat(serviceProvider[SPECIAL_PROPS.INJECT].map((dependencyDeclaration) => {
return links.concat(initializerState.initializer[SPECIAL_PROPS.INJECT].map((dependencyDeclaration) => {
const dependedServiceName = _pickServiceNameFromDeclaration(dependencyDeclaration);

@@ -303,361 +352,442 @@ return { serviceName, dependedServiceName };

async run(dependenciesDeclarations) {
const _this = this;
const internalDependencies = [
...new Set(dependenciesDeclarations.concat(DISPOSE)),
];
const siloIndex = `silo-${this._silosCounter++}`;
const siloContext = {
name: `silo-${this._silosCounter++}`,
servicesDescriptors: new Map(),
servicesSequence: [],
servicesShutdownsPromises: new Map(),
index: siloIndex,
loadingServices: [],
loadingSequences: [],
errorsPromises: [],
};
if (this.shutdownPromise) {
throw new YError(E_INSTANCE_DESTROYED);
if (this._shutdownPromise) {
throw new YError('E_INSTANCE_DESTROYED');
}
// Create a provider for the special fatal error service
siloContext.servicesDescriptors.set(FATAL_ERROR, Promise.resolve({
service: {
promise: new Promise((_resolve, reject) => {
siloContext.throwFatalError = (err) => {
debug('Handled a fatal error', err);
reject(err);
};
}),
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
siloContext.servicesDescriptors.set(SILO_CONTEXT, Promise.resolve({
service: siloContext,
}));
this._initializersStates[SILO_CONTEXT].silosInstances[siloIndex] = {
provider: { service: siloContext },
};
// Create a provider for the shutdown special dependency
siloContext.servicesDescriptors.set(DISPOSE, Promise.resolve({
service: async () => {
siloContext.shutdownPromise =
siloContext.shutdownPromise ||
_shutdownNextServices(siloContext.servicesSequence);
debug('Shutting down services');
await siloContext.shutdownPromise;
this._silosContexts.delete(siloContext);
// Shutdown services in their instanciation order
async function _shutdownNextServices(reversedServiceSequence) {
if (0 === reversedServiceSequence.length) {
return;
}
await Promise.all(reversedServiceSequence.pop().map(async (serviceName) => {
const singletonServiceDescriptor = await _this._pickupSingletonServiceDescriptorPromise(serviceName);
const serviceDescriptor = singletonServiceDescriptor ||
(await siloContext.servicesDescriptors.get(serviceName));
let serviceShutdownPromise = _this._singletonsServicesShutdownsPromises.get(serviceName) ||
siloContext.servicesShutdownsPromises.get(serviceName);
if (serviceShutdownPromise) {
debug('Reusing a service shutdown promise:', serviceName);
return serviceShutdownPromise;
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;
}
if (reversedServiceSequence.some((servicesDeclarations) => servicesDeclarations.includes(serviceName))) {
debug('Delaying service shutdown:', serviceName);
return Promise.resolve();
}
if (singletonServiceDescriptor) {
const handleSet = _this._singletonsServicesHandles.get(serviceName);
handleSet.delete(siloContext.name);
if (handleSet.size) {
debug('Singleton is used elsewhere:', serviceName, handleSet);
return Promise.resolve();
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;
}
_this._singletonsServicesDescriptors.delete(serviceName);
});
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);
}
debug('Shutting down a service:', serviceName);
serviceShutdownPromise = serviceDescriptor?.dispose
? serviceDescriptor.dispose()
: Promise.resolve();
if (singletonServiceDescriptor) {
_this._singletonsServicesShutdownsPromises.set(serviceName, serviceShutdownPromise);
}
siloContext.servicesShutdownsPromises.set(serviceName, serviceShutdownPromise);
return serviceShutdownPromise;
}));
await _shutdownNextServices(reversedServiceSequence);
}
await _shutdownNextServices(serviceLoadSequences);
}
},
dispose: Promise.resolve.bind(Promise),
},
dispose: Promise.resolve.bind(Promise),
}));
this._silosContexts.add(siloContext);
const servicesHash = await this._initializeDependencies(siloContext, siloContext.name, internalDependencies, { injectorContext: false, autoloading: false });
};
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);
return _buildFinalHash(servicesHash, dependenciesDeclarations);
debug('All dependencies now loaded:', siloContext.loadingSequences);
return services;
}
/**
* Destroy the Knifecycle instance
* @return {Promise}
* Full destruction promise
* @example
*
* import Knifecycle, { constant } from 'knifecycle'
*
* const $ = new Knifecycle();
*
* $.register(constant('ENV', process.env));
* $.run(['ENV'])
* .then(({ ENV }) => {
* // Here goes your code
*
* // Finally destroy the instance
* $.destroy()
* })
*/
async destroy() {
this.shutdownPromise =
this.shutdownPromise ||
Promise.all([...this._silosContexts].map(async (siloContext) => {
const $dispose = (await siloContext.servicesDescriptors.get(DISPOSE))
?.service;
return $dispose();
})).then(() => undefined);
debug('Shutting down Knifecycle instance.');
return this.shutdownPromise;
_getInitializer(serviceName) {
return this._initializersStates[serviceName]?.initializer;
}
/**
* Initialize or return a service descriptor
* @param {Object} siloContext
* Current execution silo context
* @param {String} serviceName
* Service name.
* @param {Object} options
* Options for service retrieval
* @param {Boolean} options.injectorContext
* Flag indicating the injection were initiated by the $injector
* @param {Boolean} options.autoloading
* Flag to indicating $autoload dependencies on the fly loading
* @param {String} serviceProvider
* Service provider.
* @return {Promise}
* Service descriptor promise.
*/
async _getServiceDescriptor(siloContext, serviceName, { injectorContext, autoloading, }) {
// Try to get service descriptior early from the silo context
let serviceDescriptorPromise = siloContext.servicesDescriptors.get(serviceName);
if (serviceDescriptorPromise) {
if (autoloading) {
debug(`⚠️ - Possible dead lock due to reusing "${serviceName}" from the silo context while autoloading.`);
_getServiceProvider(siloContext, serviceName) {
const initializerState = this._initializersStates[serviceName];
// This method expect the initialized to have a state
// so failing early if not to avoid programming errors
if (!initializerState) {
throw new YError('E_UNEXPECTED_SERVICE_READ');
}
if ('initializer' in initializerState) {
if ('singletonProvider' in initializerState) {
const provider = initializerState.singletonProvider;
if (provider) {
return provider;
}
}
return serviceDescriptorPromise;
if ('silosInstances' in initializerState &&
initializerState.silosInstances &&
initializerState.silosInstances[siloContext.index] &&
'provider' in initializerState.silosInstances[siloContext.index]) {
const provider = initializerState.silosInstances[siloContext.index].provider;
if (provider) {
return provider;
}
}
}
const initializer = await this._findInitializer(siloContext, serviceName, {
injectorContext,
autoloading,
});
serviceDescriptorPromise = this._pickupSingletonServiceDescriptorPromise(serviceName);
if (serviceDescriptorPromise) {
if (autoloading) {
debug(`⚠️ - Possible dead lock due to reusing the singleton "${serviceName}" while autoloading.`);
return;
}
async _loadInitializerDependencies(siloContext, parentsNames, dependenciesDeclarations, additionalDeclarations) {
debug(`${[...parentsNames].join('->')}: Gathering the dependencies (${dependenciesDeclarations.join(', ')}).`);
const allDependenciesDeclarations = [
...new Set(dependenciesDeclarations.concat(additionalDeclarations)),
];
const dependencies = [];
const lackingDependencies = [];
for (const serviceDeclaration of allDependenciesDeclarations) {
const { mappedName, optional } = parseDependencyDeclaration(serviceDeclaration);
const initializerState = this._initializersStates[mappedName] || {
dependents: [],
autoloaded: true,
};
this._initializersStates[mappedName] = initializerState;
initializerState.dependents.push({
silo: siloContext.index,
name: parentsNames[parentsNames.length - 1],
optional,
});
dependencies.push(mappedName);
}
do {
const previouslyLackingDependencies = [...lackingDependencies];
lackingDependencies.length = 0;
for (const mappedName of dependencies) {
if (!this._getServiceProvider(siloContext, mappedName)) {
lackingDependencies.push(mappedName);
if (!siloContext.loadingServices.includes(mappedName)) {
siloContext.loadingServices.push(mappedName);
}
}
}
this._singletonsServicesHandles.get(serviceName).add(siloContext.name);
if (lackingDependencies.length) {
await this._resolveDependencies(siloContext, lackingDependencies, parentsNames);
}
const loadSequence = previouslyLackingDependencies.filter((previouslyLackingDependency) => !lackingDependencies.includes(previouslyLackingDependency));
if (loadSequence.length) {
siloContext.loadingSequences.push(loadSequence);
}
} while (lackingDependencies.length);
return dependenciesDeclarations.reduce((finalHash, dependencyDeclaration) => {
const { serviceName, mappedName, optional } = parseDependencyDeclaration(dependencyDeclaration);
const provider = this._getServiceProvider(siloContext, mappedName);
// We expect a provider here since everything
// should be resolved
if (!provider) {
throw new YError('E_UNEXPECTED_PROVIDER_STATE', serviceName, parentsNames);
}
if (!optional && provider === NO_PROVIDER) {
throw new YError('E_UNMATCHED_DEPENDENCY', ...parentsNames, serviceName);
}
if (provider === NO_PROVIDER) {
debug(`${[...parentsNames, serviceName].join('->')}: Optional dependency not found.`);
}
finalHash[serviceName] = provider.service;
return finalHash;
}, {});
}
async _loadProvider(siloContext, serviceName, parentsNames) {
debug(`${[...parentsNames, serviceName].join('->')}: Loading the provider...`);
const initializerState = this._initializersStates[serviceName];
if (!('initializer' in initializerState) || !initializerState.initializer) {
// At that point there should be an initialiser property
throw new YError('E_UNEXPECTED_INITIALIZER_STATE', serviceName);
}
const services = await this._loadInitializerDependencies(siloContext, [...parentsNames, serviceName], initializerState.initializer[SPECIAL_PROPS.INJECT], []);
let providerPromise;
if (initializerState.initializer[SPECIAL_PROPS.TYPE] === 'service') {
const servicePromise = initializerState.initializer(services);
if (!servicePromise || !servicePromise.then) {
debug('Service initializer did not return a promise:', serviceName);
throw new YError('E_BAD_SERVICE_PROMISE', serviceName);
}
providerPromise = servicePromise.then((service) => ({ service }));
}
else if (initializerState.initializer[SPECIAL_PROPS.TYPE] === 'provider') {
providerPromise = initializerState.initializer(services);
if (!providerPromise || !providerPromise.then) {
debug('Provider initializer did not return a promise:', serviceName);
throw new YError('E_BAD_SERVICE_PROVIDER', serviceName);
}
}
else {
serviceDescriptorPromise =
siloContext.servicesDescriptors.get(serviceName);
providerPromise = Promise.reject(new YError('E_UNEXPECTED_STATE', serviceName, initializer));
}
if (serviceDescriptorPromise) {
return serviceDescriptorPromise;
if (initializerState.initializer[SPECIAL_PROPS.SINGLETON]) {
initializerState.singletonProviderLoadPromise =
providerPromise;
}
// The $injector service is mainly intended to be used as a workaround
// for unavoidable circular dependencies. It rarely make sense to
// instanciate new services at this level so printing a warning for
// debug purposes
if (injectorContext) {
debug('Warning: Instantiating a new service via the $injector. It may' +
' mean that you no longer need it if your worked around a circular' +
' dependency.');
else {
initializerState.silosInstances[siloContext.index] = {
providerLoadPromise: providerPromise,
};
}
serviceDescriptorPromise = this._initializeServiceDescriptor(siloContext, serviceName, initializer, {
autoloading: autoloading || AUTOLOAD === serviceName,
injectorContext,
});
if (initializer[SPECIAL_PROPS.SINGLETON]) {
const handlesSet = new Set();
handlesSet.add(siloContext.name);
this._singletonsServicesHandles.set(serviceName, handlesSet);
this._singletonsServicesDescriptors.set(serviceName, {
preloaded: false,
promise: serviceDescriptorPromise,
});
const provider = await providerPromise;
if (!provider ||
!(typeof provider === 'object') ||
!('service' in provider)) {
debug('Provider has no `service` property:', serviceName);
throw new YError('E_BAD_SERVICE_PROVIDER', serviceName);
}
if (provider.fatalErrorPromise) {
debug('Registering service descriptor error promise:', serviceName);
siloContext.errorsPromises.push(provider.fatalErrorPromise);
}
if (initializerState.initializer[SPECIAL_PROPS.SINGLETON]) {
initializerState.singletonProvider = provider;
}
else {
siloContext.servicesDescriptors.set(serviceName, serviceDescriptorPromise);
initializerState.silosInstances[siloContext.index] = { provider };
}
// Since the autoloader is a bit special it must be pushed here
if (AUTOLOAD === serviceName) {
siloContext.servicesSequence.unshift([AUTOLOAD]);
}
return serviceDescriptorPromise;
}
async _findInitializer(siloContext, serviceName, { injectorContext, autoloading, }) {
const initializer = this._initializers.get(serviceName);
if (initializer) {
return initializer;
}
async _getAutoloader(siloContext, parentsNames) {
// The auto loader must only have static dependencies
// and we have to do this check here to avoid caching
// non-autoloading request and then be blocked by an
// autoloader dep that waits for that cached load
if (autoloading) {
throw new YError(E_AUTOLOADER_DYNAMIC_DEPENDENCY, serviceName);
// and we have to do this check here to avoid inifinite loop
if (parentsNames.includes(AUTOLOAD)) {
debug(`${parentsNames.join('->')}: Won't try to autoload autoloader dependencies...`);
return;
}
debug('No service provider:', serviceName);
let initializerPromise = this._initializerResolvers.get(serviceName);
if (initializerPromise) {
return await initializerPromise;
const autoloaderState = this._initializersStates[AUTOLOAD];
if (!autoloaderState) {
return;
}
initializerPromise = (async () => {
if (!this._initializers.get(AUTOLOAD)) {
throw new YError(E_UNMATCHED_DEPENDENCY, serviceName);
if (!('singletonProviderLoadPromise' in autoloaderState)) {
debug(`${parentsNames.join('->')}: Instanciating the autoloader...`);
// Trick to ensure the singletonProviderLoadPromise is set
let resolveAutoloder;
autoloaderState.singletonProviderLoadPromise = new Promise((_resolve) => {
resolveAutoloder = _resolve;
});
resolveAutoloder(await this._loadProvider(siloContext, AUTOLOAD, parentsNames));
}
await autoloaderState.singletonProviderLoadPromise;
const autoloader = (await this._getServiceProvider(siloContext, AUTOLOAD));
debug(`${parentsNames.join('->')}: Loaded the autoloader...`);
if (!autoloader) {
throw new YError('E_UNEXPECTED_AUTOLOADER');
}
return autoloader.service;
}
async _loadInitializer(siloContext, serviceName, parentsNames) {
const initializerState = this._initializersStates[serviceName];
debug(`${[...parentsNames, serviceName].join('->')}: Loading an initializer...`);
// At that point there should be an initialiser state
if (!initializerState) {
throw new YError('E_UNEXPECTED_INITIALIZER_STATE', serviceName);
}
// When no initializer try to autoload it
if (!('initializer' in initializerState)) {
debug(`${[...parentsNames, serviceName].join('->')}: No registered initializer...`);
if (initializerState.initializerLoadPromise) {
debug(`${[...parentsNames, serviceName].join('->')}: Wait for pending initializer registration...`);
await initializerState.initializerLoadPromise;
}
debug(`Loading the $autoload service to lookup for: ${serviceName}.`);
try {
const autoloadingDescriptor = (await this._getServiceDescriptor(siloContext, AUTOLOAD, { injectorContext, autoloading: true }));
const { initializer, path } = await autoloadingDescriptor.service(serviceName);
if (typeof initializer !== 'function' &&
(typeof initializer !== 'object' ||
initializer[SPECIAL_PROPS.TYPE] !== 'constant')) {
throw new YError(E_BAD_AUTOLOADED_INITIALIZER, serviceName, initializer);
else {
debug(`${[...parentsNames, serviceName].join('->')}: Try to autoload the initializer...`);
initializerState.autoloaded = true;
// Trick to ensure the singletonProviderLoadPromise is set
let resolveInitializer, rejectInitializer;
initializerState.initializerLoadPromise = new Promise((_resolve, _reject) => {
resolveInitializer = _resolve;
rejectInitializer = _reject;
});
try {
const autoloader = await this._getAutoloader(siloContext, [
...parentsNames,
serviceName,
]);
if (!autoloader) {
debug(`${parentsNames.join('->')}: No autoloader found, leaving initializer undefined...`);
initializerState.initializer = undefined;
resolveInitializer(undefined);
return;
}
const result = await autoloader(serviceName);
if (typeof result !== 'object' ||
!('initializer' in result) ||
!('path' in result)) {
throw new YError('E_BAD_AUTOLOADER_RESULT', serviceName, result);
}
const { initializer, path } = result;
debug(`${[...parentsNames, serviceName].join('->')}: Loaded the initializer at path ${path}...`);
if (initializer[SPECIAL_PROPS.NAME] !== serviceName) {
throw new YError('E_AUTOLOADED_INITIALIZER_MISMATCH', serviceName, initializer[SPECIAL_PROPS.NAME]);
}
initializerState.dependents.push({
silo: siloContext.index,
name: AUTOLOAD,
optional: false,
});
initializerState.initializer = initializer;
this._buildInitializerState(initializerState, initializer);
resolveInitializer(initializer);
return;
}
if (initializer[SPECIAL_PROPS.NAME] !== serviceName) {
throw new YError(E_AUTOLOADED_INITIALIZER_MISMATCH, serviceName, initializer[SPECIAL_PROPS.NAME]);
catch (err) {
if (err.code === 'E_AULOADER_DEPENDS_ON_AUTOLOAD') {
initializerState.initializer = undefined;
rejectInitializer(err);
await initializerState.initializerLoadPromise;
return;
}
if (!['E_UNMATCHED_DEPENDENCY'].includes(err.code)) {
initializerState.initializer = undefined;
rejectInitializer(YError.wrap(err, 'E_BAD_AUTOLOADED_INITIALIZER', serviceName));
await initializerState.initializerLoadPromise;
return;
}
debug(`${[...parentsNames, serviceName].join('->')}: Could not autoload the initializer...`, err);
initializerState.initializer = undefined;
resolveInitializer(undefined);
await initializerState.initializerLoadPromise;
}
debug(`Loaded the ${serviceName} initializer at path ${path}.`);
this.register(initializer);
this._initializerResolvers.delete(serviceName);
// Here we need to pick-up the registered initializer to
// have a universally usable intitializer
return this._initializers.get(serviceName);
return;
}
catch (err) {
debug(`Could not load ${serviceName} via the auto loader.`);
throw err;
}
})();
this._initializerResolvers.set(serviceName, initializerPromise);
return await initializerPromise;
}
_pickupSingletonServiceDescriptorPromise(serviceName) {
const serviceDescriptor = this._singletonsServicesDescriptors.get(serviceName);
if (!serviceDescriptor) {
return;
}
serviceDescriptor.preloaded = false;
return serviceDescriptor.promise;
}
/**
* Initialize a service descriptor
* @param {Object} siloContext
* Current execution silo context
* @param {String} serviceName
* Service name.
* @param {Object} options
* Options for service retrieval
* @param {Boolean} options.injectorContext
* Flag indicating the injection were initiated by the $injector
* @param {Boolean} options.autoloading
* Flag to indicating $autoload dependendencies on the fly loading.
* @return {Promise}
* Service dependencies hash promise.
*/
async _initializeServiceDescriptor(siloContext, serviceName, initializer, { autoloading, injectorContext, }) {
let serviceDescriptor;
debug('Initializing a service descriptor:', serviceName);
try {
// A singleton service may use a reserved resource
// like a TCP socket. This is why we have to be aware
// of singleton services full shutdown before creating
// a new one
await (this._singletonsServicesShutdownsPromises.get(serviceName) ||
Promise.resolve());
// Anyway delete any shutdown promise before instanciating
// a new service
this._singletonsServicesShutdownsPromises.delete(serviceName);
siloContext.servicesShutdownsPromises.delete(serviceName);
const servicesHash = await this._initializeDependencies(siloContext, serviceName, initializer[SPECIAL_PROPS.INJECT], { injectorContext, autoloading });
debug('Successfully gathered service dependencies:', serviceName);
serviceDescriptor = await initializer(initializer[SPECIAL_PROPS.INJECT].reduce((finalHash, dependencyDeclaration) => {
const { serviceName, mappedName } = parseDependencyDeclaration(dependencyDeclaration);
finalHash[serviceName] = servicesHash[mappedName];
return finalHash;
}, {}));
if (!serviceDescriptor) {
debug('Provider did not return a descriptor:', serviceName);
return Promise.reject(new YError(E_BAD_SERVICE_PROVIDER, serviceName));
else {
if (initializerState.initializer) {
debug(`${[...parentsNames, serviceName].join('->')}: Initializer ready...`);
if (initializer[SPECIAL_PROPS.TYPE] === 'constant') {
const provider = initializerState.initializer[SPECIAL_PROPS.VALUE];
initializerState.singletonProvider = provider;
initializerState.singletonProviderLoadPromise = Promise.resolve(provider);
}
if (initializerState.initializer[SPECIAL_PROPS.SINGLETON]) {
const singletonInitializerState = initializerState;
if (!('singletonProviderLoadPromise' in singletonInitializerState)) {
singletonInitializerState.singletonProviderLoadPromise =
this._loadProvider(siloContext, serviceName, parentsNames);
}
await singletonInitializerState.singletonProviderLoadPromise;
}
else {
const siloedInitializerState = initializerState;
if (!siloedInitializerState.silosInstances[siloContext.index]) {
siloedInitializerState.silosInstances[siloContext.index] = {
providerLoadPromise: this._loadProvider(siloContext, serviceName, parentsNames),
};
}
await siloedInitializerState.silosInstances[siloContext.index]
.providerLoadPromise;
}
}
debug('Successfully initialized a service descriptor:', serviceName);
if (serviceDescriptor.fatalErrorPromise) {
debug('Registering service descriptor error promise:', serviceName);
siloContext.errorsPromises.push(serviceDescriptor.fatalErrorPromise);
else {
debug(`${[...parentsNames, serviceName].join('->')}: Could not find the initializer...`);
initializerState.initializer = undefined;
initializerState.singletonProvider = NO_PROVIDER;
}
siloContext.servicesDescriptors.set(serviceName, Promise.resolve(serviceDescriptor));
}
catch (err) {
debug('Error initializing a service descriptor:', serviceName, err.stack || 'no_stack_trace');
if (E_UNMATCHED_DEPENDENCY === err.code) {
throw YError.wrap(err, E_UNMATCHED_DEPENDENCY, ...[serviceName].concat(err.params));
}
async _resolveDependencies(siloContext, loadingServices, parentsNames) {
debug(`Initiating a dependencies load round for silo "${siloContext.index}"'.`);
if (this._options.sequential) {
for (const loadingService of loadingServices) {
await this._loadInitializer(siloContext, loadingService, parentsNames);
}
throw err;
}
return serviceDescriptor;
else {
await Promise.all(loadingServices.map((loadingService) => this._loadInitializer(siloContext, loadingService, parentsNames)));
}
}
/**
* Initialize a service dependencies
* @param {Object} siloContext
* Current execution silo siloContext
* @param {String} serviceName
* Service name.
* @param {String} servicesDeclarations
* Dependencies declarations.
* @param {Object} options
* Options for service retrieval
* @param {Boolean} options.injectorContext
* Flag indicating the injection were initiated by the $injector
* @param {Boolean} options.autoloading
* Flag to indicating $autoload dependendencies on the fly loading.
* Destroy the Knifecycle instance
* @return {Promise}
* Service dependencies hash promise.
* Full destruction promise
* @example
*
* import Knifecycle, { constant } from 'knifecycle'
*
* const $ = new Knifecycle();
*
* $.register(constant('ENV', process.env));
* $.run(['ENV'])
* .then(({ ENV }) => {
* // Here goes your code
*
* // Finally destroy the instance
* $.destroy()
* })
*/
async _initializeDependencies(siloContext, serviceName, servicesDeclarations, { injectorContext = false, autoloading = false, }) {
debug('Initializing dependencies:', serviceName, servicesDeclarations);
const servicesDescriptors = await Promise.all(servicesDeclarations.map(async (serviceDeclaration) => {
const { mappedName, optional } = parseDependencyDeclaration(serviceDeclaration);
try {
const serviceDescriptor = await this._getServiceDescriptor(siloContext, mappedName, {
injectorContext,
autoloading,
});
return serviceDescriptor;
}
catch (err) {
if (optional &&
[
'E_UNMATCHED_DEPENDENCY',
E_AUTOLOADER_DYNAMIC_DEPENDENCY,
].includes(err.code)) {
debug('Optional dependency not found:', serviceDeclaration, err.stack || 'no_stack_trace');
return;
}
throw err;
}
}));
debug('Initialized dependencies descriptors:', serviceName, servicesDeclarations, servicesDescriptors);
siloContext.servicesSequence.push(servicesDeclarations
.filter((_, index) => servicesDescriptors[index])
.map(_pickMappedNameFromDeclaration));
const services = await Promise.all(servicesDescriptors.map(async (serviceDescriptor) => {
if (!serviceDescriptor) {
return undefined;
}
return serviceDescriptor.service;
}));
return services.reduce((hash, service, index) => {
const mappedName = _pickMappedNameFromDeclaration(servicesDeclarations[index]);
hash[mappedName] = service;
return hash;
}, {});
async destroy() {
this._shutdownPromise =
this._shutdownPromise ||
Promise.all(Object.keys(this._silosContexts).map(async (siloIndex) => {
const siloContext = this._silosContexts[siloIndex];
const $dispose = (await this._getServiceProvider(siloContext, DISPOSE))?.service;
return $dispose();
})).then(() => undefined);
debug('Shutting down Knifecycle instance.');
return this._shutdownPromise;
}

@@ -670,6 +800,2 @@ }

}
function _pickMappedNameFromDeclaration(dependencyDeclaration) {
const { mappedName } = parseDependencyDeclaration(dependencyDeclaration);
return mappedName;
}
function _applyShapes(shapes, serviceName) {

@@ -694,3 +820,3 @@ return shapes.reduce((shapedService, shape) => {

if (!classes[style.className]) {
throw new YError(E_BAD_CLASS, style.className, serviceName);
throw new YError('E_BAD_CLASS', style.className, serviceName);
}

@@ -702,3 +828,3 @@ classesApplications[serviceName] = style.className;

if (!classes[style.className]) {
throw new YError(E_BAD_CLASS, style.className, dependedServiceName);
throw new YError('E_BAD_CLASS', style.className, dependedServiceName);
}

@@ -710,18 +836,2 @@ classesApplications[dependedServiceName] = style.className;

}
function serviceAdapter(serviceName, initializer, dependenciesHash) {
const servicePromise = initializer(dependenciesHash);
if (!servicePromise || !servicePromise.then) {
throw new YError(E_BAD_SERVICE_PROMISE, serviceName);
}
return servicePromise.then((_service_) => ({
service: _service_,
}));
}
function _buildFinalHash(servicesHash, dependenciesDeclarations) {
return dependenciesDeclarations.reduce((finalHash, dependencyDeclaration) => {
const { serviceName, mappedName } = parseDependencyDeclaration(dependencyDeclaration);
finalHash[serviceName] = servicesHash[mappedName];
return finalHash;
}, {});
}
//# sourceMappingURL=index.js.map

@@ -0,3 +1,4 @@

/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint max-nested-callbacks:0 */
import { describe, beforeEach, test } from '@jest/globals';
import { jest, describe, beforeEach, test } from '@jest/globals';
import assert from 'assert';

@@ -22,2 +23,22 @@ import sinon from 'sinon';

}
const nullService = service(async function nullService({ time }) {
// service run for its side effect only
time();
return null;
}, 'nullService', ['time']);
const undefinedService = service(async function undefinedService({ time, }) {
// service run for its side effect only
time();
return undefined;
}, 'undefinedService', ['time']);
const nullProvider = provider(async function nullProvider({ time, }) {
// provider run for its side effect only
time();
return { service: null };
}, 'nullProvider', ['time']);
const undefinedProvider = provider(async function undefinedProvider({ time, }) {
// service run for its side effect only
time();
return { service: undefined };
}, 'undefinedProvider', ['time']);
beforeEach(() => {

@@ -286,470 +307,649 @@ $ = new Knifecycle();

});
});
describe('run', () => {
test('should work with no dependencies', async () => {
const dependencies = await $.run([]);
assert.deepEqual(dependencies, {});
test('should fail with singleton depending on siloed services', () => {
assert.throws(() => {
$.register(provider(hashProvider, 'hash', [], false));
$.register(provider(hashProvider, 'hash1', ['hash'], true));
}, (err) => {
assert.deepEqual(err.code, 'E_BAD_SINGLETON_DEPENDENCIES');
assert.deepEqual(err.params, ['hash1', 'hash']);
return true;
});
});
test('should work with constant dependencies', async () => {
$.register(constant('ENV', ENV));
$.register(constant('time', time));
const dependencies = await $.run(['time', 'ENV']);
assert.deepEqual(Object.keys(dependencies), ['time', 'ENV']);
assert.deepEqual(dependencies, {
ENV,
time,
test('should fail when setting siloed services depended on by a singleton', () => {
assert.throws(() => {
$.register(provider(hashProvider, 'hash1', ['hash'], true));
$.register(provider(hashProvider, 'hash', [], false));
}, (err) => {
assert.deepEqual(err.code, 'E_BAD_SINGLETON_DEPENDENCIES');
assert.deepEqual(err.params, ['hash1', 'hash']);
return true;
});
});
test('should work with service dependencies', async () => {
const wrappedSampleService = inject(['time'], async function sampleService({ time }) {
return Promise.resolve(typeof time);
});
describe('run', () => {
describe('should work', () => {
test('with no dependencies', async () => {
const dependencies = await $.run([]);
assert.deepEqual(dependencies, {});
});
$.register(service(wrappedSampleService, 'sample'));
$.register(constant('time', time));
const dependencies = await $.run(['sample']);
assert.deepEqual(Object.keys(dependencies), ['sample']);
assert.deepEqual(dependencies, {
sample: 'function',
test('with constant dependencies', async () => {
$.register(constant('ENV', ENV));
$.register(constant('time', time));
const dependencies = await $.run(['time', 'ENV']);
assert.deepEqual(Object.keys(dependencies), ['time', 'ENV']);
assert.deepEqual(dependencies, {
ENV,
time,
});
});
});
test('should work with simple dependencies', async () => {
$.register(constant('ENV', ENV));
$.register(constant('time', time));
$.register(provider(hashProvider, 'hash', ['ENV']));
const dependencies = await $.run(['time', 'hash']);
assert.deepEqual(Object.keys(dependencies), ['time', 'hash']);
assert.deepEqual(dependencies, {
hash: { ENV },
time,
test('with service dependencies', async () => {
const wrappedSampleService = inject(['time'], async function sampleService({ time }) {
return Promise.resolve(typeof time);
});
$.register(service(wrappedSampleService, 'sample'));
$.register(constant('time', time));
const dependencies = await $.run(['sample']);
assert.deepEqual(Object.keys(dependencies), ['sample']);
assert.deepEqual(dependencies, {
sample: 'function',
});
});
});
test('should work with given optional dependencies', async () => {
$.register(constant('ENV', ENV));
$.register(constant('DEBUG', {}));
$.register(constant('time', time));
$.register(provider(hashProvider, 'hash', ['ENV', '?DEBUG']));
const dependencies = await $.run(['time', 'hash']);
assert.deepEqual(Object.keys(dependencies), ['time', 'hash']);
assert.deepEqual(dependencies, {
hash: { ENV, DEBUG: {} },
time,
test('with null service dependencies', async () => {
const time = jest.fn();
$.register(nullService);
$.register(constant('time', time));
const dependencies = await $.run(['nullService']);
assert.deepEqual(Object.keys(dependencies), ['nullService']);
assert.deepEqual(dependencies, {
nullService: null,
});
});
});
test('should work with lacking optional dependencies', async () => {
$.register(constant('ENV', ENV));
$.register(constant('time', time));
$.register(provider(hashProvider, 'hash', ['ENV', '?DEBUG']));
const dependencies = await $.run(['time', 'hash']);
assert.deepEqual(Object.keys(dependencies), ['time', 'hash']);
assert.deepEqual(dependencies, {
hash: { ENV, DEBUG: undefined },
time,
test('with null provider dependencies', async () => {
const time = jest.fn();
$.register(nullProvider);
$.register(constant('time', time));
const dependencies = await $.run(['nullProvider']);
assert.deepEqual(Object.keys(dependencies), ['nullProvider']);
assert.deepEqual(dependencies, {
nullProvider: null,
});
});
});
test('should work with deeper dependencies', async () => {
$.register(constant('ENV', ENV));
$.register(constant('time', time));
$.register(provider(hashProvider, 'hash', ['ENV']));
$.register(provider(hashProvider, 'hash1', ['hash']));
$.register(provider(hashProvider, 'hash2', ['hash1']));
$.register(provider(hashProvider, 'hash3', ['hash2']));
$.register(provider(hashProvider, 'hash4', ['hash3']));
$.register(provider(hashProvider, 'hash5', ['hash4']));
const dependencies = await $.run(['hash5', 'time']);
assert.deepEqual(Object.keys(dependencies), ['hash5', 'time']);
});
test('should instanciate services once', async () => {
const timeServiceStub = sinon.spy(timeService);
$.register(constant('ENV', ENV));
$.register(service(timeServiceStub, 'time'));
$.register(provider(hashProvider, 'hash', ['ENV', 'time']));
$.register(provider(hashProvider, 'hash2', ['ENV', 'time']));
$.register(provider(hashProvider, 'hash3', ['ENV', 'time']));
const dependencies = await $.run([
'hash',
'hash2',
'hash3',
'time',
]);
assert.deepEqual(Object.keys(dependencies), [
'hash',
'hash2',
'hash3',
'time',
]);
assert.deepEqual(timeServiceStub.args, [[{}]]);
});
test('should instanciate a single mapped service', async () => {
const providerStub = sinon.stub().returns(Promise.resolve({
service: 'stub',
}));
const providerStub2 = sinon.stub().returns(Promise.resolve({
service: 'stub2',
}));
$.register(provider(providerStub, 'mappedStub', ['stub2>mappedStub2']));
$.register(provider(providerStub2, 'mappedStub2'));
const dependencies = await $.run([
'stub>mappedStub',
]);
assert.deepEqual(dependencies, {
stub: 'stub',
test('with undefined dependencies', async () => {
const time = jest.fn();
$.register(undefinedService);
$.register(undefinedProvider);
$.register(constant('time', time));
const dependencies = await $.run([
'undefinedService',
'undefinedProvider',
]);
assert.deepEqual(Object.keys(dependencies), [
'undefinedService',
'undefinedProvider',
]);
assert.deepEqual(dependencies, {
undefinedService: undefined,
undefinedProvider: undefined,
});
});
assert.deepEqual(providerStub.args, [
[
{
stub2: 'stub2',
},
],
]);
});
test('should instanciate several services with mappings', async () => {
const timeServiceStub = sinon.spy(timeService);
$.register(constant('ENV', ENV));
$.register(singleton(service(timeServiceStub, 'aTime')));
$.register(provider(hashProvider, 'aHash', ['ENV', 'time>aTime']));
$.register(provider(hashProvider, 'aHash2', ['ENV', 'hash>aHash']));
$.register(provider(hashProvider, 'aHash3', ['ENV', 'hash>aHash']));
const dependencies = await $.run([
'hash2>aHash2',
'hash3>aHash3',
'time>aTime',
]);
assert.deepEqual(Object.keys(dependencies), ['hash2', 'hash3', 'time']);
assert.deepEqual(timeServiceStub.args, [[{}]]);
});
test('should fail with bad service', async () => {
$.register(service((() => undefined), 'lol'));
try {
await $.run(['lol']);
throw new Error('E_UNEXPECTED_SUCCESS');
}
catch (err) {
assert.deepEqual(err.code, 'E_BAD_SERVICE_PROMISE');
assert.deepEqual(err.params, ['lol']);
}
});
test('should fail with bad provider', async () => {
$.register(provider((() => undefined), 'lol'));
try {
await $.run(['lol']);
throw new Error('E_UNEXPECTED_SUCCESS');
}
catch (err) {
assert.deepEqual(err.code, 'E_BAD_SERVICE_PROVIDER');
assert.deepEqual(err.params, ['lol']);
}
});
test('should fail with bad service in a provider', async () => {
$.register(provider(() => Promise.resolve(), 'lol'));
try {
await $.run(['lol']);
throw new Error('E_UNEXPECTED_SUCCESS');
}
catch (err) {
assert.deepEqual(err.code, 'E_BAD_SERVICE_PROVIDER');
assert.deepEqual(err.params, ['lol']);
}
});
test('should fail with undeclared dependencies', async () => {
try {
await $.run(['lol']);
throw new Error('E_UNEXPECTED_SUCCESS');
}
catch (err) {
assert.deepEqual(err.code, 'E_UNMATCHED_DEPENDENCY');
assert.deepEqual(err.params, ['lol']);
}
});
test('should fail with undeclared dependencies upstream', async () => {
$.register(constant('ENV', ENV));
$.register(constant('time', time));
$.register(provider(hashProvider, 'hash', ['ENV', 'hash2']));
$.register(provider(hashProvider, 'hash2', ['ENV', 'lol']));
try {
await $.run(['time', 'hash']);
throw new Error('E_UNEXPECTED_SUCCESS');
}
catch (err) {
assert.deepEqual(err.code, 'E_UNMATCHED_DEPENDENCY');
assert.deepEqual(err.params, ['hash', 'hash2', 'lol']);
}
});
test('should provide a fatal error handler', async () => {
$.register(constant('ENV', ENV));
$.register(constant('time', time));
$.register(provider(hashProvider, 'hash', ['ENV']));
$.register(provider(dbProvider, 'db', ['ENV']));
$.register(provider(processProvider, 'process', ['$fatalError']));
function processProvider({ $fatalError, }) {
return Promise.resolve({
service: {
fatalErrorPromise: $fatalError.promise,
},
test('with simple dependencies', async () => {
$.register(constant('ENV', ENV));
$.register(constant('time', time));
$.register(provider(hashProvider, 'hash', ['ENV']));
const dependencies = await $.run(['time', 'hash']);
assert.deepEqual(Object.keys(dependencies), ['time', 'hash']);
assert.deepEqual(dependencies, {
hash: { ENV },
time,
});
}
async function dbProvider({ ENV }) {
let service;
const fatalErrorPromise = new Promise((resolve, reject) => {
service = Promise.resolve({
resolve,
reject,
ENV,
});
});
test('with given optional dependencies', async () => {
$.register(constant('ENV', ENV));
$.register(constant('DEBUG', {}));
$.register(constant('time', time));
$.register(provider(hashProvider, 'hash', ['ENV', '?DEBUG']));
const dependencies = await $.run(['time', 'hash']);
assert.deepEqual(Object.keys(dependencies), ['time', 'hash']);
assert.deepEqual(dependencies, {
hash: { ENV, DEBUG: {} },
time,
});
return {
service,
fatalErrorPromise,
};
}
const { process, db } = await $.run([
'time',
'hash',
'db',
'process',
]);
try {
db.reject(new Error('E_DB_ERROR'));
await process.fatalErrorPromise;
throw new Error('E_UNEXPECTED_SUCCESS');
}
catch (err) {
assert.deepEqual(err.message, 'E_DB_ERROR');
}
});
test('with lacking optional dependencies', async () => {
$.register(constant('ENV', ENV));
$.register(constant('time', time));
$.register(provider(hashProvider, 'hash', ['ENV', '?DEBUG']));
const dependencies = await $.run(['time', 'hash']);
assert.deepEqual(Object.keys(dependencies), ['time', 'hash']);
assert.deepEqual(dependencies, {
hash: { ENV, DEBUG: undefined },
time,
});
});
test('with deeper dependencies', async () => {
$.register(constant('ENV', ENV));
$.register(constant('time', time));
$.register(provider(hashProvider, 'hash', ['ENV']));
$.register(provider(hashProvider, 'hash1', ['hash']));
$.register(provider(hashProvider, 'hash2', ['hash1']));
$.register(provider(hashProvider, 'hash3', ['hash2']));
$.register(provider(hashProvider, 'hash4', ['hash3']));
$.register(provider(hashProvider, 'hash5', ['hash4']));
const dependencies = await $.run(['hash5', 'time']);
assert.deepEqual(Object.keys(dependencies), ['hash5', 'time']);
});
test('and instanciate services once', async () => {
const timeServiceStub = sinon.spy(timeService);
$.register(constant('ENV', ENV));
$.register(service(timeServiceStub, 'time'));
$.register(provider(hashProvider, 'hash', ['ENV', 'time']));
$.register(provider(hashProvider, 'hash2', ['ENV', 'time']));
$.register(provider(hashProvider, 'hash3', ['ENV', 'time']));
const dependencies = await $.run([
'hash',
'hash2',
'hash3',
'time',
]);
assert.deepEqual(Object.keys(dependencies), [
'hash',
'hash2',
'hash3',
'time',
]);
assert.deepEqual(timeServiceStub.args, [[{}]]);
});
test('and instanciate a single mapped service', async () => {
const providerStub = sinon.stub().returns(Promise.resolve({
service: 'stub',
}));
const providerStub2 = sinon.stub().returns(Promise.resolve({
service: 'stub2',
}));
$.register(provider(providerStub, 'mappedStub', ['stub2>mappedStub2']));
$.register(provider(providerStub2, 'mappedStub2'));
const dependencies = await $.run(['stub>mappedStub']);
assert.deepEqual(dependencies, {
stub: 'stub',
});
assert.deepEqual(providerStub.args, [
[
{
stub2: 'stub2',
},
],
]);
});
test('and instanciate several services with mappings', async () => {
const timeServiceStub = sinon.spy(timeService);
$.register(constant('ENV', ENV));
$.register(singleton(service(timeServiceStub, 'aTime')));
$.register(provider(hashProvider, 'aHash', ['ENV', 'time>aTime']));
$.register(provider(hashProvider, 'aHash2', ['ENV', 'hash>aHash']));
$.register(provider(hashProvider, 'aHash3', ['ENV', 'hash>aHash']));
const dependencies = await $.run([
'hash2>aHash2',
'hash3>aHash3',
'time>aTime',
]);
assert.deepEqual(Object.keys(dependencies), ['hash2', 'hash3', 'time']);
assert.deepEqual(timeServiceStub.args, [[{}]]);
});
});
describe('should fail', () => {
test('with bad service', async () => {
$.register(service((() => undefined), 'lol'));
try {
await $.run(['lol']);
throw new Error('E_UNEXPECTED_SUCCESS');
}
catch (err) {
assert.deepEqual(err.code, 'E_BAD_SERVICE_PROMISE');
assert.deepEqual(err.params, ['lol']);
}
});
test('with bad provider', async () => {
$.register(provider((() => undefined), 'lol'));
try {
await $.run(['lol']);
throw new Error('E_UNEXPECTED_SUCCESS');
}
catch (err) {
assert.deepEqual(err.code, 'E_BAD_SERVICE_PROVIDER');
assert.deepEqual(err.params, ['lol']);
}
});
test('with bad service in a provider', async () => {
$.register(provider(() => Promise.resolve(), 'lol'));
try {
await $.run(['lol']);
throw new Error('E_UNEXPECTED_SUCCESS');
}
catch (err) {
assert.deepEqual(err.code, 'E_BAD_SERVICE_PROVIDER');
assert.deepEqual(err.params, ['lol']);
}
});
test('with undeclared dependencies', async () => {
try {
await $.run(['lol']);
throw new Error('E_UNEXPECTED_SUCCESS');
}
catch (err) {
assert.deepEqual(err.code, 'E_UNMATCHED_DEPENDENCY');
assert.deepEqual(err.params, ['__run__', 'lol']);
}
});
test('with undeclared dependencies upstream', async () => {
$.register(constant('ENV', ENV));
$.register(constant('time', time));
$.register(provider(hashProvider, 'hash', ['ENV', 'hash2']));
$.register(provider(hashProvider, 'hash2', ['ENV', 'lol']));
try {
await $.run(['time', 'hash']);
throw new Error('E_UNEXPECTED_SUCCESS');
}
catch (err) {
assert.deepEqual(err.code, 'E_UNMATCHED_DEPENDENCY');
assert.deepEqual(err.params, [
'__run__',
'hash',
'hash2',
'lol',
]);
}
});
test('and provide a fatal error handler', async () => {
$.register(constant('ENV', ENV));
$.register(constant('time', time));
$.register(provider(hashProvider, 'hash', ['ENV']));
$.register(provider(dbProvider, 'db', ['ENV']));
$.register(provider(processProvider, 'process', ['$fatalError']));
function processProvider({ $fatalError, }) {
return Promise.resolve({
service: {
fatalErrorPromise: $fatalError.promise,
},
});
}
async function dbProvider({ ENV }) {
let service;
const fatalErrorPromise = new Promise((resolve, reject) => {
service = {
resolve,
reject,
ENV,
};
});
return {
service,
fatalErrorPromise,
};
}
const { process, db } = await $.run([
'time',
'hash',
'db',
'process',
]);
try {
db.reject(new Error('E_DB_ERROR'));
await process.fatalErrorPromise;
throw new Error('E_UNEXPECTED_SUCCESS');
}
catch (err) {
assert.deepEqual(err.message, 'E_DB_ERROR');
}
});
});
});
describe('autoload', () => {
test('should work with lacking autoloaded dependencies', async () => {
const autoloaderInitializer = initializer({
type: 'service',
name: '$autoload',
inject: [],
singleton: true,
}, async () => async (serviceName) => ({
path: '/path/of/debug',
initializer: initializer({
describe('should work', () => {
test('with constant dependencies', async () => {
const autoloaderInitializer = initializer({
type: 'service',
name: 'DEBUG',
name: '$autoload',
inject: [],
}, async () => 'THE_DEBUG:' + serviceName),
}));
const wrappedProvider = provider(hashProvider, 'hash', ['ENV', '?DEBUG']);
$.register(autoloaderInitializer);
$.register(wrappedProvider);
$.register(constant('ENV', ENV));
$.register(constant('time', time));
const dependencies = await $.run(['time', 'hash']);
assert.deepEqual(Object.keys(dependencies), ['time', 'hash']);
assert.deepEqual(dependencies, {
hash: { ENV, DEBUG: 'THE_DEBUG:DEBUG' },
time,
singleton: true,
}, async () => async (serviceName) => ({
path: `/path/of/${serviceName}`,
initializer: constant(serviceName, `value_of:${serviceName}`),
}));
const wrappedProvider = provider(hashProvider, 'hash', [
'ENV',
'?DEBUG',
]);
$.register(autoloaderInitializer);
$.register(wrappedProvider);
const dependencies = await $.run(['time', 'hash']);
assert.deepEqual(Object.keys(dependencies), ['time', 'hash']);
assert.deepEqual(dependencies, {
hash: { ENV: 'value_of:ENV', DEBUG: 'value_of:DEBUG' },
time: 'value_of:time',
});
});
});
test('should work with deeper several lacking dependencies', async () => {
$.register(initializer({
name: '$autoload',
type: 'service',
singleton: true,
}, async () => async (serviceName) => ({
path: `/path/to/${serviceName}`,
initializer: initializer({
type: 'provider',
name: serviceName,
inject: 'hash2' === serviceName
? ['hash1']
: 'hash4' === serviceName
? ['hash3']
: [],
}, hashProvider),
})));
$.register(constant('ENV', ENV));
$.register(constant('time', time));
$.register(provider(hashProvider, 'hash', ['ENV']));
$.register(provider(hashProvider, 'hash1', ['hash']));
$.register(provider(hashProvider, 'hash3', ['hash2']));
$.register(provider(hashProvider, 'hash5', ['hash4']));
const dependencies = await $.run(['hash5', 'time']);
assert.deepEqual(Object.keys(dependencies), ['hash5', 'time']);
});
test('should work with various dependencies', async () => {
$.register(provider(hashProvider, 'hash', ['hash2']));
$.register(provider(hashProvider, 'hash3', ['?ENV']));
$.register(constant('DEBUG', 1));
$.register(initializer({
type: 'service',
name: '$autoload',
inject: ['?ENV', 'DEBUG'],
singleton: true,
}, async () => async (serviceName) => {
if ('ENV' === serviceName) {
throw new YError('E_UNMATCHED_DEPENDENCY');
}
return {
test('with lacking autoloaded dependencies', async () => {
const autoloaderInitializer = initializer({
type: 'service',
name: '$autoload',
inject: [],
singleton: true,
}, async () => async (serviceName) => ({
path: '/path/of/debug',
initializer: initializer({
type: 'service',
name: 'hash2',
inject: ['hash3'],
}, async () => 'THE_HASH:' + serviceName),
};
}));
const dependencies = await $.run(['hash', '?ENV']);
assert.deepEqual(Object.keys(dependencies), ['hash', 'ENV']);
name: 'DEBUG',
inject: [],
}, async () => 'THE_DEBUG:' + serviceName),
}));
const wrappedProvider = provider(hashProvider, 'hash', [
'ENV',
'?DEBUG',
]);
$.register(autoloaderInitializer);
$.register(wrappedProvider);
$.register(constant('ENV', ENV));
$.register(constant('time', time));
const dependencies = await $.run(['time', 'hash']);
assert.deepEqual(Object.keys(dependencies), ['time', 'hash']);
assert.deepEqual(dependencies, {
hash: { ENV, DEBUG: 'THE_DEBUG:DEBUG' },
time,
});
});
test('with deeper several lacking dependencies', async () => {
$.register(initializer({
name: '$autoload',
type: 'service',
singleton: true,
}, async () => async (serviceName) => ({
path: `/path/to/${serviceName}`,
initializer: initializer({
type: 'provider',
name: serviceName,
inject: 'hash2' === serviceName
? ['hash1']
: 'hash4' === serviceName
? ['hash3']
: [],
}, hashProvider),
})));
$.register(constant('ENV', ENV));
$.register(constant('time', time));
$.register(provider(hashProvider, 'hash', ['ENV']));
$.register(provider(hashProvider, 'hash1', ['hash']));
$.register(provider(hashProvider, 'hash3', ['hash2']));
$.register(provider(hashProvider, 'hash5', ['hash4']));
const dependencies = await $.run(['hash5', 'time']);
assert.deepEqual(Object.keys(dependencies), ['hash5', 'time']);
});
test('with various dependencies', async () => {
$.register(provider(hashProvider, 'hash', ['hash2']));
$.register(provider(hashProvider, 'hash3', ['?ENV']));
$.register(constant('DEBUG', 1));
$.register(initializer({
type: 'service',
name: '$autoload',
inject: ['DEBUG'],
singleton: true,
}, async () => async (serviceName) => {
if ('ENV' === serviceName) {
throw new YError('E_UNMATCHED_DEPENDENCY');
}
return {
path: '/path/of/debug',
initializer: initializer({
type: 'service',
name: 'hash2',
inject: ['hash3'],
}, async () => 'THE_HASH:' + serviceName),
};
}));
const dependencies = await $.run(['hash', '?ENV']);
assert.deepEqual(Object.keys(dependencies), ['hash', 'ENV']);
});
test('and instanciate services once', async () => {
$.register(initializer({
name: '$autoload',
type: 'service',
singleton: true,
}, async () => async (serviceName) => ({
path: `/path/to/${serviceName}`,
initializer: initializer({
type: 'provider',
name: serviceName,
inject: ['ENV', 'time'],
}, hashProvider),
})));
const timeServiceStub = sinon.spy(timeService);
$.register(constant('ENV', ENV));
$.register(service(timeServiceStub, 'time'));
$.register(provider(hashProvider, 'hash', ['hash1', 'hash2', 'hash3']));
$.register(provider(hashProvider, 'hash_', ['hash1', 'hash2', 'hash3']));
const dependencies = await $.run(['hash', 'hash_', 'hash3']);
assert.deepEqual(timeServiceStub.args, [[{}]]);
assert.deepEqual(Object.keys(dependencies), ['hash', 'hash_', 'hash3']);
});
test('with null service dependencies', async () => {
const time = jest.fn();
$.register(constant('time', time));
$.register(initializer({
name: '$autoload',
type: 'service',
singleton: true,
}, async () => async (serviceName) => ({
path: `/path/to/${serviceName}`,
initializer: nullService,
})));
const dependencies = await $.run(['nullService']);
assert.deepEqual(Object.keys(dependencies), ['nullService']);
assert.deepEqual(dependencies, {
nullService: null,
});
});
test('with null provider dependencies', async () => {
const time = jest.fn();
$.register(constant('time', time));
$.register(initializer({
name: '$autoload',
type: 'service',
singleton: true,
}, async () => async (serviceName) => ({
path: `/path/to/${serviceName}`,
initializer: nullProvider,
})));
const dependencies = await $.run(['nullProvider']);
assert.deepEqual(Object.keys(dependencies), ['nullProvider']);
assert.deepEqual(dependencies, {
nullProvider: null,
});
});
test('with undefined dependencies', async () => {
const time = jest.fn();
$.register(initializer({
name: '$autoload',
type: 'service',
singleton: true,
}, async () => async (serviceName) => ({
path: `/path/to/${serviceName}`,
initializer: serviceName === 'undefinedService'
? undefinedService
: undefinedProvider,
})));
$.register(constant('time', time));
const dependencies = await $.run([
'undefinedService',
'undefinedProvider',
]);
assert.deepEqual(Object.keys(dependencies), [
'undefinedService',
'undefinedProvider',
]);
assert.deepEqual(dependencies, {
undefinedService: undefined,
undefinedProvider: null,
});
});
test('when autoload depends on optional and unexisting autoloaded dependencies', async () => {
$.register(initializer({
type: 'service',
name: '$autoload',
inject: ['?ENV'],
singleton: true,
}, async () => async (serviceName) => ({
path: `/path/of/${serviceName}`,
initializer: initializer({
type: 'service',
name: serviceName,
inject: [],
}, async () => `THE_${serviceName.toUpperCase()}:` + serviceName),
})));
const dependencies = await $.run(['test']);
assert.deepEqual(Object.keys(dependencies), ['test']);
});
test('when autoload depends on deeper optional and unexisting autoloaded dependencies', async () => {
$.register(initializer({
type: 'service',
name: 'log',
inject: ['?LOG_ROUTING', '?LOGGER', '?debug'],
singleton: true,
}, async () => {
return () => undefined;
}));
$.register(constant('LOGGER', 'LOGGER_CONSTANT'));
$.register(constant('debug', 'debug_value'));
$.register(initializer({
type: 'service',
name: '$autoload',
inject: ['?ENV', '?log'],
singleton: true,
}, async () => async (serviceName) => ({
path: `/path/of/${serviceName}`,
initializer: initializer({
type: 'service',
name: serviceName,
inject: [],
}, async () => `THE_${serviceName.toUpperCase()}:` + serviceName),
})));
const dependencies = await $.run(['test', 'log']);
assert.deepEqual(Object.keys(dependencies), ['test', 'log']);
});
});
test('should instanciate services once', async () => {
$.register(initializer({
name: '$autoload',
type: 'service',
singleton: true,
}, async () => async (serviceName) => ({
path: `/path/to/${serviceName}`,
initializer: initializer({
type: 'provider',
name: serviceName,
inject: ['ENV', 'time'],
}, hashProvider),
})));
const timeServiceStub = sinon.spy(timeService);
$.register(constant('ENV', ENV));
$.register(service(timeServiceStub, 'time'));
$.register(provider(hashProvider, 'hash', ['hash1', 'hash2', 'hash3']));
$.register(provider(hashProvider, 'hash_', ['hash1', 'hash2', 'hash3']));
const dependencies = await $.run([
'hash',
'hash_',
'hash3',
]);
assert.deepEqual(timeServiceStub.args, [[{}]]);
assert.deepEqual(Object.keys(dependencies), ['hash', 'hash_', 'hash3']);
});
test('should fail when autoload does not exists', async () => {
try {
await $.run(['test']);
throw new YError('E_UNEXPECTED_SUCCESS');
}
catch (err) {
assert.equal(err.code, 'E_UNMATCHED_DEPENDENCY');
}
});
test('should fail when autoloaded dependencies are not found', async () => {
$.register(initializer({
type: 'service',
name: '$autoload',
inject: [],
singleton: true,
}, async () => async (serviceName) => {
throw new YError('E_CANNOT_AUTOLOAD', serviceName);
}));
try {
await $.run(['test']);
throw new YError('E_UNEXPECTED_SUCCESS');
}
catch (err) {
assert.equal(err.code, 'E_CANNOT_AUTOLOAD');
assert.deepEqual(err.params, ['test']);
}
});
test('should fail when autoloaded dependencies are not initializers', async () => {
$.register(initializer({
type: 'service',
name: '$autoload',
inject: [],
singleton: true,
}, async () => async () => 'not_an_initializer'));
try {
await $.run(['test']);
throw new YError('E_UNEXPECTED_SUCCESS');
}
catch (err) {
assert.equal(err.code, 'E_BAD_AUTOLOADED_INITIALIZER');
assert.deepEqual(err.params, ['test', undefined]);
}
});
test('should fail when autoloaded dependencies are not right initializers', async () => {
$.register(initializer({
type: 'service',
name: '$autoload',
inject: [],
singleton: true,
}, async () => async (serviceName) => ({
path: '/path/of/debug',
initializer: initializer({
describe('should fail', () => {
test('when autoload does not exists', async () => {
try {
await $.run(['test']);
throw new YError('E_UNEXPECTED_SUCCESS');
}
catch (err) {
assert.equal(err.code, 'E_UNMATCHED_DEPENDENCY');
}
});
test('when autoloaded dependencies are not found', async () => {
$.register(initializer({
type: 'service',
name: 'not-' + serviceName,
name: '$autoload',
inject: [],
}, async () => 'THE_TEST:' + serviceName),
})));
try {
await $.run(['test']);
throw new YError('E_UNEXPECTED_SUCCESS');
}
catch (err) {
assert.equal(err.code, 'E_AUTOLOADED_INITIALIZER_MISMATCH');
assert.deepEqual(err.params, ['test', 'not-test']);
}
});
test('should fail when autoload depends on existing autoloaded dependencies', async () => {
$.register(initializer({
type: 'service',
name: '$autoload',
inject: ['ENV'],
singleton: true,
}, async () => async (serviceName) => ({
path: '/path/of/debug',
initializer: initializer({
singleton: true,
}, async () => async (serviceName) => {
throw new YError('E_CANNOT_AUTOLOAD', serviceName);
}));
try {
await $.run(['test']);
throw new YError('E_UNEXPECTED_SUCCESS');
}
catch (err) {
assert.equal(err.code, 'E_BAD_AUTOLOADED_INITIALIZER');
assert.deepEqual(err.params, ['test']);
assert.equal(err.wrappedErrors[0].code, 'E_CANNOT_AUTOLOAD');
assert.deepEqual(err.wrappedErrors[0].params, ['test']);
}
});
test('when the autoloader returns bad data', async () => {
$.register(initializer({
type: 'service',
name: 'DEBUG',
name: '$autoload',
inject: [],
}, async () => 'THE_DEBUG:' + serviceName),
})));
try {
await $.run(['test']);
throw new YError('E_UNEXPECTED_SUCCESS');
}
catch (err) {
assert.equal(err.code, 'E_AUTOLOADER_DYNAMIC_DEPENDENCY');
assert.deepEqual(err.params, ['ENV']);
}
});
test('should work when autoload depends on optional and unexisting autoloaded dependencies', async () => {
$.register(initializer({
type: 'service',
name: '$autoload',
inject: ['?ENV'],
singleton: true,
}, async () => async (serviceName) => ({
path: `/path/of/${serviceName}`,
initializer: initializer({
singleton: true,
}, async () => async () => 'not_an_initializer'));
try {
await $.run(['test']);
throw new YError('E_UNEXPECTED_SUCCESS');
}
catch (err) {
assert.equal(err.code, 'E_BAD_AUTOLOADED_INITIALIZER');
assert.deepEqual(err.params, ['test']);
assert.equal(err.wrappedErrors[0].code, 'E_BAD_AUTOLOADER_RESULT');
assert.deepEqual(err.wrappedErrors[0].params, ['test', 'not_an_initializer']);
}
});
test('when autoloaded dependencies are not initializers', async () => {
$.register(initializer({
type: 'service',
name: serviceName,
name: '$autoload',
inject: [],
}, async () => `THE_${serviceName.toUpperCase()}:` + serviceName),
})));
const dependencies = await $.run(['test']);
assert.deepEqual(Object.keys(dependencies), ['test']);
});
test.skip('should work when autoload depends on deeper optional and unexisting autoloaded dependencies', async () => {
$.register(initializer({
type: 'service',
name: 'log',
inject: ['?LOG_ROUTING', '?LOGGER', '?debug'],
}, async () => {
return () => undefined;
}));
$.register(constant('LOGGER', 'LOGGER_CONSTANT'));
$.register(constant('debug', 'debug_value'));
$.register(initializer({
type: 'service',
name: '$autoload',
inject: ['?ENV', '?log'],
singleton: true,
}, async () => async (serviceName) => ({
path: `/path/of/${serviceName}`,
initializer: initializer({
singleton: true,
}, async () => async () => ({
initializer: 'not_an_initializer',
path: '/path/to/initializer',
})));
try {
await $.run(['test']);
throw new YError('E_UNEXPECTED_SUCCESS');
}
catch (err) {
assert.equal(err.code, 'E_BAD_AUTOLOADED_INITIALIZER');
assert.deepEqual(err.params, ['test']);
assert.equal(err.wrappedErrors[0].code, 'E_AUTOLOADED_INITIALIZER_MISMATCH');
assert.deepEqual(err.wrappedErrors[0].params, ['test', undefined]);
}
});
test('when autoloaded dependencies are not right initializers', async () => {
$.register(initializer({
type: 'service',
name: serviceName,
name: '$autoload',
inject: [],
}, async () => `THE_${serviceName.toUpperCase()}:` + serviceName),
})));
const dependencies = await $.run(['test', 'log']);
assert.deepEqual(Object.keys(dependencies), ['test', 'log']);
singleton: true,
}, async () => async (serviceName) => ({
path: '/path/of/debug',
initializer: initializer({
type: 'service',
name: 'not-' + serviceName,
inject: [],
}, async () => 'THE_TEST:' + serviceName),
})));
try {
await $.run(['test']);
throw new YError('E_UNEXPECTED_SUCCESS');
}
catch (err) {
assert.equal(err.code, 'E_BAD_AUTOLOADED_INITIALIZER');
assert.deepEqual(err.params, ['test']);
assert.equal(err.wrappedErrors[0].code, 'E_AUTOLOADED_INITIALIZER_MISMATCH');
assert.deepEqual(err.wrappedErrors[0].params, ['test', 'not-test']);
}
});
test('when autoload depends on existing autoloaded dependencies', async () => {
$.register(initializer({
type: 'service',
name: '$autoload',
inject: ['ENV'],
singleton: true,
}, async () => async (serviceName) => ({
path: '/path/of/debug',
initializer: initializer({
type: 'service',
name: 'DEBUG',
inject: [],
}, async () => 'THE_DEBUG:' + serviceName),
})));
try {
await $.run(['test']);
throw new YError('E_UNEXPECTED_SUCCESS');
}
catch (err) {
assert.equal(err.code, 'E_UNMATCHED_DEPENDENCY');
assert.deepEqual(err.params, ['__run__', 'test']);
}
});
});

@@ -762,7 +962,3 @@ });

$.register(provider(hashProvider, 'hash', ['ENV']));
const dependencies = await $.run([
'time',
'hash',
'$injector',
]);
const dependencies = await $.run(['time', 'hash', '$injector']);
assert.deepEqual(Object.keys(dependencies), [

@@ -781,7 +977,3 @@ 'time',

$.register(provider(hashProvider, 'hash', ['ENV']));
const dependencies = await $.run([
'time',
'hash',
'$injector',
]);
const dependencies = await $.run(['time', 'hash', '$injector']);
assert.deepEqual(Object.keys(dependencies), [

@@ -803,7 +995,3 @@ 'time',

$.register(provider(hashProvider, 'hash', ['ENV']));
const dependencies = await $.run([
'time',
'hash',
'$injector',
]);
const dependencies = await $.run(['time', 'hash', '$injector']);
assert.deepEqual(Object.keys(dependencies), [

@@ -828,6 +1016,3 @@ 'time',

$.register(provider(hashProvider, 'hash', ['ENV']));
const dependencies = await $.run([
'time',
'$injector',
]);
const dependencies = await $.run(['time', '$injector']);
assert.deepEqual(Object.keys(dependencies), ['time', '$injector']);

@@ -896,9 +1081,3 @@ const injectDependencies = await dependencies.$injector(['time', 'hash']);

$.run(['$instance']),
$.run([
'$instance',
'ENV',
'hash',
'hash1',
'time',
]),
$.run(['$instance', 'ENV', 'hash', 'hash1', 'time']),
$.run(['$instance', 'ENV', 'hash', 'hash2']),

@@ -916,9 +1095,3 @@ ]);

$.run(['$instance']),
$.run([
'$dispose',
'ENV',
'hash',
'hash1',
'time',
]),
$.run(['$dispose', 'ENV', 'hash', 'hash1', 'time']),
$.run(['ENV', 'hash', 'hash2']),

@@ -957,7 +1130,3 @@ ]);

$.register(constant('time', time));
const dependencies = await $.run([
'time',
'ENV',
'$dispose',
]);
const dependencies = await $.run(['time', 'ENV', '$dispose']);
assert.deepEqual(Object.keys(dependencies), ['time', 'ENV', '$dispose']);

@@ -970,7 +1139,3 @@ await dependencies.$dispose();

$.register(provider(hashProvider, 'hash', ['ENV']));
const dependencies = await $.run([
'time',
'hash',
'$dispose',
]);
const dependencies = await $.run(['time', 'hash', '$dispose']);
assert.deepEqual(Object.keys(dependencies), ['time', 'hash', '$dispose']);

@@ -1081,6 +1246,3 @@ await dependencies.$dispose();

}), 'hash2', ['hash1', 'hash']));
const dependencies = await $.run([
'hash2',
'$dispose',
]);
const dependencies = await $.run(['hash2', '$dispose']);
assert.deepEqual(Object.keys(dependencies), ['hash2', '$dispose']);

@@ -1099,13 +1261,6 @@ await dependencies.$dispose();

const { hash } = await $.run(['time', 'hash']);
const dependencies = await $.run([
'time',
'hash',
'$dispose',
]);
const dependencies = await $.run(['time', 'hash', '$dispose']);
assert.equal(dependencies.hash, hash);
await dependencies.$dispose();
const newDependencies = await $.run([
'time',
'hash',
]);
const newDependencies = await $.run(['time', 'hash']);
assert.equal(newDependencies.hash, hash);

@@ -1117,7 +1272,3 @@ });

$.register(provider(hashProvider, 'hash', ['ENV'], true));
const { hash, $dispose } = await $.run([
'time',
'hash',
'$dispose',
]);
const { hash, $dispose } = await $.run(['time', 'hash', '$dispose']);
await $dispose();

@@ -1124,0 +1275,0 @@ const dependencies = await $.run(['time', 'hash']);

@@ -0,1 +1,2 @@

/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint @typescript-eslint/ban-types:0 */

@@ -2,0 +3,0 @@ import { YError } from 'yerror';

@@ -0,1 +1,3 @@

/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */
import { describe, test } from '@jest/globals';

@@ -2,0 +4,0 @@ import assert from 'assert';

@@ -28,3 +28,3 @@ {

"name": "knifecycle",
"version": "15.0.1",
"version": "16.0.0",
"description": "Manage your NodeJS processes's lifecycle automatically with an unobtrusive dependency injection implementation.",

@@ -84,27 +84,27 @@ "main": "dist/index.js",

"@swc/cli": "^0.1.62",
"@swc/core": "^1.3.60",
"@swc/core": "^1.3.76",
"@swc/helpers": "^0.5.1",
"@swc/jest": "^0.2.26",
"@typescript-eslint/eslint-plugin": "^5.59.7",
"@typescript-eslint/parser": "^5.59.7",
"@swc/jest": "^0.2.28",
"@typescript-eslint/eslint-plugin": "^6.3.0",
"@typescript-eslint/parser": "^6.3.0",
"commitizen": "^4.3.0",
"conventional-changelog-cli": "^2.2.2",
"conventional-changelog-cli": "^3.0.0",
"coveralls": "^3.1.1",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^8.41.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^4.2.1",
"jest": "^29.5.0",
"eslint": "^8.46.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"jest": "^29.6.2",
"jsarch": "^6.0.1",
"jsdoc-to-markdown": "^8.0.0",
"metapak": "^5.0.1",
"metapak-nfroidure": "^14.1.3",
"prettier": "^2.8.8",
"metapak": "^5.1.3",
"metapak-nfroidure": "^15.0.0",
"prettier": "^3.0.1",
"rimraf": "^5.0.1",
"sinon": "^14.0.0",
"typescript": "^5.0.4"
"typescript": "^5.1.6"
},
"dependencies": {
"debug": "^4.3.4",
"yerror": "^6.2.1"
"yerror": "^7.0.0"
},

@@ -202,3 +202,4 @@ "config": {

".ts"
]
],
"prettierPath": null
},

@@ -205,0 +206,0 @@ "jsarch": {

@@ -10,4 +10,4 @@ [//]: # ( )

[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/nfroidure/knifecycle/blob/master/LICENSE)
[![Coverage Status](https://coveralls.io/repos/github/nfroidure/knifecycle/badge.svg?branch=master)](https://coveralls.io/github/nfroidure/knifecycle?branch=master)
[![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)

@@ -453,3 +453,3 @@

* [Knifecycle](#Knifecycle)
* [new Knifecycle()](#new_Knifecycle_new)
* [new Knifecycle(options)](#new_Knifecycle_new)
* [.register(initializer)](#Knifecycle+register) ⇒ [<code>Knifecycle</code>](#Knifecycle)

@@ -459,12 +459,15 @@ * [.toMermaidGraph(options)](#Knifecycle+toMermaidGraph) ⇒ <code>String</code>

* [.destroy()](#Knifecycle+destroy) ⇒ <code>Promise</code>
* [._getServiceDescriptor(siloContext, serviceName, options, serviceProvider)](#Knifecycle+_getServiceDescriptor) ⇒ <code>Promise</code>
* [._initializeServiceDescriptor(siloContext, serviceName, options)](#Knifecycle+_initializeServiceDescriptor) ⇒ <code>Promise</code>
* [._initializeDependencies(siloContext, serviceName, servicesDeclarations, options)](#Knifecycle+_initializeDependencies) ⇒ <code>Promise</code>
<a name="new_Knifecycle_new"></a>
### new Knifecycle()
### new Knifecycle(options)
Create a new Knifecycle instance
**Returns**: [<code>Knifecycle</code>](#Knifecycle) - The Knifecycle instance
| Param | Type | Description |
| --- | --- | --- |
| options | <code>Object</code> | An object with options |
| options.sequential | <code>boolean</code> | Allows to load dependencies sequentially (usefull for debugging) |
**Example**

@@ -567,52 +570,2 @@ ```js

```
<a name="Knifecycle+_getServiceDescriptor"></a>
### knifecycle.\_getServiceDescriptor(siloContext, serviceName, options, serviceProvider) ⇒ <code>Promise</code>
Initialize or return a service descriptor
**Kind**: instance method of [<code>Knifecycle</code>](#Knifecycle)
**Returns**: <code>Promise</code> - Service descriptor promise.
| Param | Type | Description |
| --- | --- | --- |
| siloContext | <code>Object</code> | Current execution silo context |
| serviceName | <code>String</code> | Service name. |
| options | <code>Object</code> | Options for service retrieval |
| options.injectorContext | <code>Boolean</code> | Flag indicating the injection were initiated by the $injector |
| options.autoloading | <code>Boolean</code> | Flag to indicating $autoload dependencies on the fly loading |
| serviceProvider | <code>String</code> | Service provider. |
<a name="Knifecycle+_initializeServiceDescriptor"></a>
### knifecycle.\_initializeServiceDescriptor(siloContext, serviceName, options) ⇒ <code>Promise</code>
Initialize a service descriptor
**Kind**: instance method of [<code>Knifecycle</code>](#Knifecycle)
**Returns**: <code>Promise</code> - Service dependencies hash promise.
| Param | Type | Description |
| --- | --- | --- |
| siloContext | <code>Object</code> | Current execution silo context |
| serviceName | <code>String</code> | Service name. |
| options | <code>Object</code> | Options for service retrieval |
| options.injectorContext | <code>Boolean</code> | Flag indicating the injection were initiated by the $injector |
| options.autoloading | <code>Boolean</code> | Flag to indicating $autoload dependendencies on the fly loading. |
<a name="Knifecycle+_initializeDependencies"></a>
### knifecycle.\_initializeDependencies(siloContext, serviceName, servicesDeclarations, options) ⇒ <code>Promise</code>
Initialize a service dependencies
**Kind**: instance method of [<code>Knifecycle</code>](#Knifecycle)
**Returns**: <code>Promise</code> - Service dependencies hash promise.
| Param | Type | Description |
| --- | --- | --- |
| siloContext | <code>Object</code> | Current execution silo siloContext |
| serviceName | <code>String</code> | Service name. |
| servicesDeclarations | <code>String</code> | Dependencies declarations. |
| options | <code>Object</code> | Options for service retrieval |
| options.injectorContext | <code>Boolean</code> | Flag indicating the injection were initiated by the $injector |
| options.autoloading | <code>Boolean</code> | Flag to indicating $autoload dependendencies on the fly loading. |
<a name="initInitializerBuilder"></a>

@@ -908,2 +861,2 @@

# License
[MIT](https://github.com/nfroidure/knifecycle/blob/master/LICENSE)
[MIT](https://github.com/nfroidure/knifecycle/blob/main/LICENSE)

@@ -0,1 +1,2 @@

/* eslint-disable @typescript-eslint/no-explicit-any */
import { describe, test } from '@jest/globals';

@@ -6,3 +7,2 @@ import assert from 'assert';

import { Knifecycle, initializer, constant } from './index.js';
import type { BuildInitializer } from './build.js';

@@ -68,5 +68,3 @@ describe('buildInitializer', () => {

const { buildInitializer } = await $.run<{
buildInitializer: BuildInitializer;
}>(['buildInitializer']);
const { buildInitializer } = await $.run<any>(['buildInitializer']);

@@ -142,2 +140,89 @@ const content = await buildInitializer(['dep1', 'finalMappedDep>dep3']);

});
// TODO: allow building with internal dependencies
test.skip('should work with simple internal services dependencies', async () => {
const $ = new Knifecycle();
$.register(constant('PWD', '~/my-project'));
$.register(initAutoloader);
$.register(initInitializerBuilder);
$.register(constant('$fatalError', {}));
const { buildInitializer } = await $.run<any>(['buildInitializer']);
const content = await buildInitializer([
'dep1',
'finalMappedDep>dep3',
'$fatalError',
'$dispose',
'$siloContext',
]);
assert.equal(
content,
`
// Definition batch #0
import initDep1 from './services/dep1';
const NODE_ENV = "development";
// Definition batch #1
import initDep2 from './services/dep2';
// Definition batch #2
import initDep3 from './services/dep3';
export async function initialize(services = {}) {
// Initialization batch #0
const batch0 = {
dep1: initDep1({
}),
NODE_ENV: Promise.resolve(NODE_ENV),
};
await Promise.all(
Object.keys(batch0)
.map(key => batch0[key])
);
services['dep1'] = await batch0['dep1'];
services['NODE_ENV'] = await batch0['NODE_ENV'];
// Initialization batch #1
const batch1 = {
dep2: initDep2({
dep1: services['dep1'],
NODE_ENV: services['NODE_ENV'],
}).then(provider => provider.service),
};
await Promise.all(
Object.keys(batch1)
.map(key => batch1[key])
);
services['dep2'] = await batch1['dep2'];
// Initialization batch #2
const batch2 = {
dep3: initDep3({
dep2: services['dep2'],
dep1: services['dep1'],
depOpt: services['depOpt'],
}),
};
await Promise.all(
Object.keys(batch2)
.map(key => batch2[key])
);
services['dep3'] = await batch2['dep3'];
return {
dep1: services['dep1'],
finalMappedDep: services['dep3'],
};
}
`,
);
});
});

@@ -139,23 +139,22 @@ import {

const batch${index} = {${batch
.map((name) => {
if (
'constant' ===
dependenciesHash[name].__initializer[SPECIAL_PROPS.TYPE]
) {
return `
.map((name) => {
if (
'constant' === dependenciesHash[name].__initializer[SPECIAL_PROPS.TYPE]
) {
return `
${name}: Promise.resolve(${name}),`;
}
return `
}
return `
${name}: ${dependenciesHash[name].__initializerName}({${
dependenciesHash[name].__inject
? `${dependenciesHash[name].__inject
.map(parseDependencyDeclaration)
.map(
({ serviceName, mappedName }) =>
`
dependenciesHash[name].__inject
? `${dependenciesHash[name].__inject
.map(parseDependencyDeclaration)
.map(
({ serviceName, mappedName }) =>
`
${serviceName}: services['${mappedName}'],`,
)
.join('')}`
: ''
}
)
.join('')}`
: ''
}
})${

@@ -166,4 +165,4 @@ 'provider' === dependenciesHash[name].__type

},`;
})
.join('')}
})
.join('')}
};

@@ -170,0 +169,0 @@

@@ -0,3 +1,4 @@

/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint max-nested-callbacks:0 */
import { describe, beforeEach, test } from '@jest/globals';
import { jest, describe, beforeEach, test } from '@jest/globals';
import assert from 'assert';

@@ -16,4 +17,4 @@ import sinon from 'sinon';

singleton,
FatalErrorService,
} from './index.js';
import type { Provider, FatalErrorService } from './index.js';
import { ALLOWED_INITIALIZER_TYPES } from './util.js';

@@ -38,2 +39,51 @@

const nullService = service<{ time: any }, null>(
async function nullService({ time }: { time: any }): Promise<null> {
// service run for its side effect only
time();
return null;
},
'nullService',
['time'],
);
const undefinedService = service<{ time: any }, undefined>(
async function undefinedService({
time,
}: {
time: any;
}): Promise<undefined> {
// service run for its side effect only
time();
return undefined;
},
'undefinedService',
['time'],
);
const nullProvider = provider<{ time: any }, null>(
async function nullProvider({
time,
}: {
time: any;
}): Promise<Provider<null>> {
// provider run for its side effect only
time();
return { service: null };
},
'nullProvider',
['time'],
);
const undefinedProvider = provider<{ time: any }, undefined>(
async function undefinedProvider({
time,
}: {
time: any;
}): Promise<Provider<undefined>> {
// service run for its side effect only
time();
return { service: undefined };
},
'undefinedProvider',
['time'],
);
beforeEach(() => {

@@ -56,3 +106,3 @@ $ = new Knifecycle();

$.register(constant('TEST', 2));
assert.deepEqual(await $.run<Record<string, any>>(['TEST']), {
assert.deepEqual(await $.run<any>(['TEST']), {
TEST: 2,

@@ -64,3 +114,3 @@ });

$.register(constant('TEST', 1));
assert.deepEqual(await $.run<Record<string, any>>(['TEST']), {
assert.deepEqual(await $.run<any>(['TEST']), {
TEST: 1,

@@ -90,3 +140,3 @@ });

const { test } = await $.run<{ test: () => number }>(['test']);
const { test } = await $.run<any>(['test']);
assert.deepEqual(test(), 2);

@@ -97,3 +147,3 @@ });

$.register(service(async () => () => 1, 'test'));
const { test } = await $.run<{ test: () => number }>(['test']);
const { test } = await $.run<any>(['test']);
assert.deepEqual(test(), 1);

@@ -144,3 +194,3 @@

const { test } = await $.run<Record<string, any>>(['test']);
const { test } = await $.run<any>(['test']);
assert.deepEqual(test, 2);

@@ -176,3 +226,3 @@ });

const { test } = await $.run<Record<string, any>>(['test']);
const { test } = await $.run<any>(['test']);
assert.deepEqual(test, 2);

@@ -196,3 +246,3 @@ });

const { test } = await $.run<Record<string, any>>(['test']);
const { test } = await $.run<any>(['test']);
assert.deepEqual(test, 1);

@@ -419,448 +469,380 @@

});
});
describe('run', () => {
test('should work with no dependencies', async () => {
const dependencies = await $.run<Record<string, any>>([]);
assert.deepEqual(dependencies, {});
test('should fail with singleton depending on siloed services', () => {
assert.throws(
() => {
$.register(provider(hashProvider, 'hash', [], false));
$.register(provider(hashProvider, 'hash1', ['hash'], true));
},
(err) => {
assert.deepEqual(
(err as YError).code,
'E_BAD_SINGLETON_DEPENDENCIES',
);
assert.deepEqual((err as YError).params, ['hash1', 'hash']);
return true;
},
);
});
test('should work with constant dependencies', async () => {
$.register(constant('ENV', ENV));
$.register(constant('time', time));
const dependencies = await $.run<Record<string, any>>(['time', 'ENV']);
assert.deepEqual(Object.keys(dependencies), ['time', 'ENV']);
assert.deepEqual(dependencies, {
ENV,
time,
});
});
test('should work with service dependencies', async () => {
const wrappedSampleService = inject<{ time: any }, string>(
['time'],
async function sampleService({ time }: { time: any }) {
return Promise.resolve(typeof time);
test('should fail when setting siloed services depended on by a singleton', () => {
assert.throws(
() => {
$.register(provider(hashProvider, 'hash1', ['hash'], true));
$.register(provider(hashProvider, 'hash', [], false));
},
(err) => {
assert.deepEqual(
(err as YError).code,
'E_BAD_SINGLETON_DEPENDENCIES',
);
assert.deepEqual((err as YError).params, ['hash1', 'hash']);
return true;
},
);
$.register(service(wrappedSampleService, 'sample'));
$.register(constant('time', time));
});
});
const dependencies = await $.run<Record<string, any>>(['sample']);
describe('run', () => {
describe('should work', () => {
test('with no dependencies', async () => {
const dependencies = await $.run<any>([]);
assert.deepEqual(Object.keys(dependencies), ['sample']);
assert.deepEqual(dependencies, {
sample: 'function',
assert.deepEqual(dependencies, {});
});
});
test('should work with simple dependencies', async () => {
$.register(constant('ENV', ENV));
$.register(constant('time', time));
$.register(provider(hashProvider, 'hash', ['ENV']));
test('with constant dependencies', async () => {
$.register(constant('ENV', ENV));
$.register(constant('time', time));
const dependencies = await $.run<Record<string, any>>(['time', 'hash']);
const dependencies = await $.run<any>(['time', 'ENV']);
assert.deepEqual(Object.keys(dependencies), ['time', 'hash']);
assert.deepEqual(dependencies, {
hash: { ENV },
time,
assert.deepEqual(Object.keys(dependencies), ['time', 'ENV']);
assert.deepEqual(dependencies, {
ENV,
time,
});
});
});
test('should work with given optional dependencies', async () => {
$.register(constant('ENV', ENV));
$.register(constant('DEBUG', {}));
$.register(constant('time', time));
$.register(provider(hashProvider, 'hash', ['ENV', '?DEBUG']));
test('with service dependencies', async () => {
const wrappedSampleService = inject<{ time: any }, string>(
['time'],
async function sampleService({ time }: { time: any }) {
return Promise.resolve(typeof time);
},
);
$.register(service(wrappedSampleService, 'sample'));
$.register(constant('time', time));
const dependencies = await $.run<Record<string, any>>(['time', 'hash']);
const dependencies = await $.run<any>(['sample']);
assert.deepEqual(Object.keys(dependencies), ['time', 'hash']);
assert.deepEqual(dependencies, {
hash: { ENV, DEBUG: {} },
time,
assert.deepEqual(Object.keys(dependencies), ['sample']);
assert.deepEqual(dependencies, {
sample: 'function',
});
});
});
test('should work with lacking optional dependencies', async () => {
$.register(constant('ENV', ENV));
$.register(constant('time', time));
$.register(provider(hashProvider, 'hash', ['ENV', '?DEBUG']));
test('with null service dependencies', async () => {
const time = jest.fn();
const dependencies = await $.run<Record<string, any>>(['time', 'hash']);
$.register(nullService);
$.register(constant('time', time));
assert.deepEqual(Object.keys(dependencies), ['time', 'hash']);
assert.deepEqual(dependencies, {
hash: { ENV, DEBUG: undefined },
time,
const dependencies = await $.run<any>(['nullService']);
assert.deepEqual(Object.keys(dependencies), ['nullService']);
assert.deepEqual(dependencies, {
nullService: null,
});
});
});
test('should work with deeper dependencies', async () => {
$.register(constant('ENV', ENV));
$.register(constant('time', time));
$.register(provider(hashProvider, 'hash', ['ENV']));
$.register(provider(hashProvider, 'hash1', ['hash']));
$.register(provider(hashProvider, 'hash2', ['hash1']));
$.register(provider(hashProvider, 'hash3', ['hash2']));
$.register(provider(hashProvider, 'hash4', ['hash3']));
$.register(provider(hashProvider, 'hash5', ['hash4']));
test('with null provider dependencies', async () => {
const time = jest.fn();
const dependencies = await $.run<Record<string, any>>(['hash5', 'time']);
$.register(nullProvider);
$.register(constant('time', time));
assert.deepEqual(Object.keys(dependencies), ['hash5', 'time']);
});
const dependencies = await $.run<any>(['nullProvider']);
test('should instanciate services once', async () => {
const timeServiceStub = sinon.spy(timeService);
assert.deepEqual(Object.keys(dependencies), ['nullProvider']);
assert.deepEqual(dependencies, {
nullProvider: null,
});
});
$.register(constant('ENV', ENV));
$.register(service(timeServiceStub, 'time'));
$.register(provider(hashProvider, 'hash', ['ENV', 'time']));
$.register(provider(hashProvider, 'hash2', ['ENV', 'time']));
$.register(provider(hashProvider, 'hash3', ['ENV', 'time']));
test('with undefined dependencies', async () => {
const time = jest.fn();
const dependencies = await $.run<Record<string, any>>([
'hash',
'hash2',
'hash3',
'time',
]);
$.register(undefinedService);
$.register(undefinedProvider);
$.register(constant('time', time));
assert.deepEqual(Object.keys(dependencies), [
'hash',
'hash2',
'hash3',
'time',
]);
assert.deepEqual(timeServiceStub.args, [[{}]]);
});
const dependencies = await $.run<any>([
'undefinedService',
'undefinedProvider',
]);
test('should instanciate a single mapped service', async () => {
const providerStub = sinon.stub().returns(
Promise.resolve({
service: 'stub',
}),
);
const providerStub2 = sinon.stub().returns(
Promise.resolve({
service: 'stub2',
}),
);
assert.deepEqual(Object.keys(dependencies), [
'undefinedService',
'undefinedProvider',
]);
assert.deepEqual(dependencies, {
undefinedService: undefined,
undefinedProvider: undefined,
});
});
$.register(provider(providerStub, 'mappedStub', ['stub2>mappedStub2']));
$.register(provider(providerStub2, 'mappedStub2'));
test('with simple dependencies', async () => {
$.register(constant('ENV', ENV));
$.register(constant('time', time));
$.register(provider(hashProvider, 'hash', ['ENV']));
const dependencies = await $.run<Record<string, any>>([
'stub>mappedStub',
]);
const dependencies = await $.run<any>(['time', 'hash']);
assert.deepEqual(dependencies, {
stub: 'stub',
assert.deepEqual(Object.keys(dependencies), ['time', 'hash']);
assert.deepEqual(dependencies, {
hash: { ENV },
time,
});
});
assert.deepEqual(providerStub.args, [
[
{
stub2: 'stub2',
},
],
]);
});
test('should instanciate several services with mappings', async () => {
const timeServiceStub = sinon.spy(timeService);
test('with given optional dependencies', async () => {
$.register(constant('ENV', ENV));
$.register(constant('DEBUG', {}));
$.register(constant('time', time));
$.register(provider(hashProvider, 'hash', ['ENV', '?DEBUG']));
$.register(constant('ENV', ENV));
$.register(singleton(service(timeServiceStub, 'aTime')));
$.register(provider(hashProvider, 'aHash', ['ENV', 'time>aTime']));
$.register(provider(hashProvider, 'aHash2', ['ENV', 'hash>aHash']));
$.register(provider(hashProvider, 'aHash3', ['ENV', 'hash>aHash']));
const dependencies = await $.run<any>(['time', 'hash']);
const dependencies = await $.run<Record<string, any>>([
'hash2>aHash2',
'hash3>aHash3',
'time>aTime',
]);
assert.deepEqual(Object.keys(dependencies), ['time', 'hash']);
assert.deepEqual(dependencies, {
hash: { ENV, DEBUG: {} },
time,
});
});
assert.deepEqual(Object.keys(dependencies), ['hash2', 'hash3', 'time']);
assert.deepEqual(timeServiceStub.args, [[{}]]);
});
test('with lacking optional dependencies', async () => {
$.register(constant('ENV', ENV));
$.register(constant('time', time));
$.register(provider(hashProvider, 'hash', ['ENV', '?DEBUG']));
test('should fail with bad service', async () => {
$.register(service((() => undefined) as any, 'lol'));
const dependencies = await $.run<any>(['time', 'hash']);
try {
await $.run<Record<string, any>>(['lol']);
throw new Error('E_UNEXPECTED_SUCCESS');
} catch (err) {
assert.deepEqual((err as YError).code, 'E_BAD_SERVICE_PROMISE');
assert.deepEqual((err as YError).params, ['lol']);
}
});
assert.deepEqual(Object.keys(dependencies), ['time', 'hash']);
assert.deepEqual(dependencies, {
hash: { ENV, DEBUG: undefined },
time,
});
});
test('should fail with bad provider', async () => {
$.register(provider((() => undefined) as any, 'lol'));
try {
await $.run<Record<string, any>>(['lol']);
throw new Error('E_UNEXPECTED_SUCCESS');
} catch (err) {
assert.deepEqual((err as YError).code, 'E_BAD_SERVICE_PROVIDER');
assert.deepEqual((err as YError).params, ['lol']);
}
});
test('with deeper dependencies', async () => {
$.register(constant('ENV', ENV));
$.register(constant('time', time));
$.register(provider(hashProvider, 'hash', ['ENV']));
$.register(provider(hashProvider, 'hash1', ['hash']));
$.register(provider(hashProvider, 'hash2', ['hash1']));
$.register(provider(hashProvider, 'hash3', ['hash2']));
$.register(provider(hashProvider, 'hash4', ['hash3']));
$.register(provider(hashProvider, 'hash5', ['hash4']));
test('should fail with bad service in a provider', async () => {
$.register(provider(() => Promise.resolve() as any, 'lol'));
try {
await $.run<Record<string, any>>(['lol']);
throw new Error('E_UNEXPECTED_SUCCESS');
} catch (err) {
assert.deepEqual((err as YError).code, 'E_BAD_SERVICE_PROVIDER');
assert.deepEqual((err as YError).params, ['lol']);
}
});
const dependencies = await $.run<any>(['hash5', 'time']);
test('should fail with undeclared dependencies', async () => {
try {
await $.run<Record<string, any>>(['lol']);
throw new Error('E_UNEXPECTED_SUCCESS');
} catch (err) {
assert.deepEqual((err as YError).code, 'E_UNMATCHED_DEPENDENCY');
assert.deepEqual((err as YError).params, ['lol']);
}
});
assert.deepEqual(Object.keys(dependencies), ['hash5', 'time']);
});
test('should fail with undeclared dependencies upstream', async () => {
$.register(constant('ENV', ENV));
$.register(constant('time', time));
$.register(provider(hashProvider, 'hash', ['ENV', 'hash2']));
$.register(provider(hashProvider, 'hash2', ['ENV', 'lol']));
test('and instanciate services once', async () => {
const timeServiceStub = sinon.spy(timeService);
try {
await $.run<Record<string, any>>(['time', 'hash']);
throw new Error('E_UNEXPECTED_SUCCESS');
} catch (err) {
assert.deepEqual((err as YError).code, 'E_UNMATCHED_DEPENDENCY');
assert.deepEqual((err as YError).params, ['hash', 'hash2', 'lol']);
}
});
$.register(constant('ENV', ENV));
$.register(service(timeServiceStub, 'time'));
$.register(provider(hashProvider, 'hash', ['ENV', 'time']));
$.register(provider(hashProvider, 'hash2', ['ENV', 'time']));
$.register(provider(hashProvider, 'hash3', ['ENV', 'time']));
test('should provide a fatal error handler', async () => {
$.register(constant('ENV', ENV));
$.register(constant('time', time));
$.register(provider(hashProvider, 'hash', ['ENV']));
$.register(provider(dbProvider, 'db', ['ENV']));
$.register(provider(processProvider, 'process', ['$fatalError']));
const dependencies = await $.run<any>([
'hash',
'hash2',
'hash3',
'time',
]);
function processProvider({
$fatalError,
}: {
$fatalError: FatalErrorService;
}) {
return Promise.resolve({
service: {
fatalErrorPromise: $fatalError.promise,
},
});
}
assert.deepEqual(Object.keys(dependencies), [
'hash',
'hash2',
'hash3',
'time',
]);
assert.deepEqual(timeServiceStub.args, [[{}]]);
});
async function dbProvider({ ENV }: { ENV: Record<string, string> }) {
let service;
const fatalErrorPromise = new Promise<void>((resolve, reject) => {
service = Promise.resolve({
resolve,
reject,
ENV,
});
});
test('and instanciate a single mapped service', async () => {
const providerStub = sinon.stub().returns(
Promise.resolve({
service: 'stub',
}),
);
const providerStub2 = sinon.stub().returns(
Promise.resolve({
service: 'stub2',
}),
);
return {
service,
fatalErrorPromise,
};
}
$.register(provider(providerStub, 'mappedStub', ['stub2>mappedStub2']));
$.register(provider(providerStub2, 'mappedStub2'));
const { process, db } = await $.run<Record<string, any>>([
'time',
'hash',
'db',
'process',
]);
const dependencies = await $.run<any>(['stub>mappedStub']);
try {
db.reject(new Error('E_DB_ERROR'));
await process.fatalErrorPromise;
throw new Error('E_UNEXPECTED_SUCCESS');
} catch (err) {
assert.deepEqual((err as Error).message, 'E_DB_ERROR');
}
});
});
describe('autoload', () => {
test('should work with lacking autoloaded dependencies', async () => {
const autoloaderInitializer = initializer(
{
type: 'service',
name: '$autoload',
inject: [],
singleton: true,
},
async () => async (serviceName) => ({
path: '/path/of/debug',
initializer: initializer(
assert.deepEqual(dependencies, {
stub: 'stub',
});
assert.deepEqual(providerStub.args, [
[
{
type: 'service',
name: 'DEBUG',
inject: [],
stub2: 'stub2',
},
async () => 'THE_DEBUG:' + serviceName,
),
}),
);
const wrappedProvider = provider(hashProvider, 'hash', ['ENV', '?DEBUG']);
],
]);
});
$.register(autoloaderInitializer);
$.register(wrappedProvider);
$.register(constant('ENV', ENV));
$.register(constant('time', time));
test('and instanciate several services with mappings', async () => {
const timeServiceStub = sinon.spy(timeService);
const dependencies = await $.run<Record<string, any>>(['time', 'hash']);
$.register(constant('ENV', ENV));
$.register(singleton(service(timeServiceStub, 'aTime')));
$.register(provider(hashProvider, 'aHash', ['ENV', 'time>aTime']));
$.register(provider(hashProvider, 'aHash2', ['ENV', 'hash>aHash']));
$.register(provider(hashProvider, 'aHash3', ['ENV', 'hash>aHash']));
assert.deepEqual(Object.keys(dependencies), ['time', 'hash']);
assert.deepEqual(dependencies, {
hash: { ENV, DEBUG: 'THE_DEBUG:DEBUG' },
time,
const dependencies = await $.run<any>([
'hash2>aHash2',
'hash3>aHash3',
'time>aTime',
]);
assert.deepEqual(Object.keys(dependencies), ['hash2', 'hash3', 'time']);
assert.deepEqual(timeServiceStub.args, [[{}]]);
});
});
describe('should fail', () => {
test('with bad service', async () => {
$.register(service((() => undefined) as any, 'lol'));
test('should work with deeper several lacking dependencies', async () => {
$.register(
initializer(
{
name: '$autoload',
type: 'service',
singleton: true,
},
async () => async (serviceName) => ({
path: `/path/to/${serviceName}`,
initializer: initializer(
{
type: 'provider',
name: serviceName,
inject:
'hash2' === serviceName
? ['hash1']
: 'hash4' === serviceName
? ['hash3']
: [],
},
hashProvider,
),
}),
),
);
$.register(constant('ENV', ENV));
$.register(constant('time', time));
$.register(provider(hashProvider, 'hash', ['ENV']));
$.register(provider(hashProvider, 'hash1', ['hash']));
$.register(provider(hashProvider, 'hash3', ['hash2']));
$.register(provider(hashProvider, 'hash5', ['hash4']));
try {
await $.run<any>(['lol']);
throw new Error('E_UNEXPECTED_SUCCESS');
} catch (err) {
assert.deepEqual((err as YError).code, 'E_BAD_SERVICE_PROMISE');
assert.deepEqual((err as YError).params, ['lol']);
}
});
const dependencies = await $.run<Record<string, any>>(['hash5', 'time']);
test('with bad provider', async () => {
$.register(provider((() => undefined) as any, 'lol'));
try {
await $.run<any>(['lol']);
throw new Error('E_UNEXPECTED_SUCCESS');
} catch (err) {
assert.deepEqual((err as YError).code, 'E_BAD_SERVICE_PROVIDER');
assert.deepEqual((err as YError).params, ['lol']);
}
});
assert.deepEqual(Object.keys(dependencies), ['hash5', 'time']);
});
test('with bad service in a provider', async () => {
$.register(provider(() => Promise.resolve() as any, 'lol'));
try {
await $.run<any>(['lol']);
throw new Error('E_UNEXPECTED_SUCCESS');
} catch (err) {
assert.deepEqual((err as YError).code, 'E_BAD_SERVICE_PROVIDER');
assert.deepEqual((err as YError).params, ['lol']);
}
});
test('should work with various dependencies', async () => {
$.register(provider(hashProvider, 'hash', ['hash2']));
$.register(provider(hashProvider, 'hash3', ['?ENV']));
$.register(constant('DEBUG', 1));
$.register(
initializer(
{
type: 'service',
name: '$autoload',
inject: ['?ENV', 'DEBUG'],
singleton: true,
},
async () => async (serviceName) => {
if ('ENV' === serviceName) {
throw new YError('E_UNMATCHED_DEPENDENCY');
}
test('with undeclared dependencies', async () => {
try {
await $.run<any>(['lol']);
throw new Error('E_UNEXPECTED_SUCCESS');
} catch (err) {
assert.deepEqual((err as YError).code, 'E_UNMATCHED_DEPENDENCY');
assert.deepEqual((err as YError).params, ['__run__', 'lol']);
}
});
return {
path: '/path/of/debug',
initializer: initializer(
{
type: 'service',
name: 'hash2',
inject: ['hash3'],
},
async () => 'THE_HASH:' + serviceName,
),
};
},
),
);
test('with undeclared dependencies upstream', async () => {
$.register(constant('ENV', ENV));
$.register(constant('time', time));
$.register(provider(hashProvider, 'hash', ['ENV', 'hash2']));
$.register(provider(hashProvider, 'hash2', ['ENV', 'lol']));
const dependencies = await $.run<Record<string, any>>(['hash', '?ENV']);
try {
await $.run<any>(['time', 'hash']);
throw new Error('E_UNEXPECTED_SUCCESS');
} catch (err) {
assert.deepEqual((err as YError).code, 'E_UNMATCHED_DEPENDENCY');
assert.deepEqual((err as YError).params, [
'__run__',
'hash',
'hash2',
'lol',
]);
}
});
assert.deepEqual(Object.keys(dependencies), ['hash', 'ENV']);
});
test('and provide a fatal error handler', async () => {
$.register(constant('ENV', ENV));
$.register(constant('time', time));
$.register(provider(hashProvider, 'hash', ['ENV']));
$.register(provider(dbProvider, 'db', ['ENV']));
$.register(provider(processProvider, 'process', ['$fatalError']));
test('should instanciate services once', async () => {
$.register(
initializer(
{
name: '$autoload',
type: 'service',
singleton: true,
},
async () => async (serviceName) => ({
path: `/path/to/${serviceName}`,
initializer: initializer(
{
type: 'provider',
name: serviceName,
inject: ['ENV', 'time'],
},
hashProvider,
),
}),
),
);
const timeServiceStub = sinon.spy(timeService);
function processProvider({
$fatalError,
}: {
$fatalError: FatalErrorService;
}) {
return Promise.resolve({
service: {
fatalErrorPromise: $fatalError.promise,
},
});
}
$.register(constant('ENV', ENV));
$.register(service(timeServiceStub, 'time'));
$.register(provider(hashProvider, 'hash', ['hash1', 'hash2', 'hash3']));
$.register(provider(hashProvider, 'hash_', ['hash1', 'hash2', 'hash3']));
async function dbProvider({ ENV }: { ENV: Record<string, string> }) {
let service;
const fatalErrorPromise = new Promise<void>((resolve, reject) => {
service = {
resolve,
reject,
ENV,
};
});
const dependencies = await $.run<Record<string, any>>([
'hash',
'hash_',
'hash3',
]);
return {
service,
fatalErrorPromise,
};
}
assert.deepEqual(timeServiceStub.args, [[{}]]);
assert.deepEqual(Object.keys(dependencies), ['hash', 'hash_', 'hash3']);
});
const { process, db } = await $.run<any>([
'time',
'hash',
'db',
'process',
]);
test('should fail when autoload does not exists', async () => {
try {
await $.run<Record<string, any>>(['test']);
throw new YError('E_UNEXPECTED_SUCCESS');
} catch (err) {
assert.equal((err as YError).code, 'E_UNMATCHED_DEPENDENCY');
}
try {
db.reject(new Error('E_DB_ERROR'));
await process.fatalErrorPromise;
throw new Error('E_UNEXPECTED_SUCCESS');
} catch (err) {
assert.deepEqual((err as Error).message, 'E_DB_ERROR');
}
});
});
});
test('should fail when autoloaded dependencies are not found', async () => {
$.register(
initializer(
describe('autoload', () => {
describe('should work', () => {
test('with constant dependencies', async () => {
const autoloaderInitializer = initializer(
{

@@ -872,42 +854,26 @@ type: 'service',

},
async () => async (serviceName) => {
throw new YError('E_CANNOT_AUTOLOAD', serviceName);
},
),
);
async () => async (serviceName) => ({
path: `/path/of/${serviceName}`,
initializer: constant(serviceName, `value_of:${serviceName}`),
}),
);
const wrappedProvider = provider(hashProvider, 'hash', [
'ENV',
'?DEBUG',
]);
try {
await $.run<Record<string, any>>(['test']);
throw new YError('E_UNEXPECTED_SUCCESS');
} catch (err) {
assert.equal((err as YError).code, 'E_CANNOT_AUTOLOAD');
assert.deepEqual((err as YError).params, ['test']);
}
});
$.register(autoloaderInitializer);
$.register(wrappedProvider);
test('should fail when autoloaded dependencies are not initializers', async () => {
$.register(
initializer(
{
type: 'service',
name: '$autoload',
inject: [],
singleton: true,
},
async () => async () => 'not_an_initializer',
),
);
const dependencies = await $.run<any>(['time', 'hash']);
try {
await $.run<Record<string, any>>(['test']);
throw new YError('E_UNEXPECTED_SUCCESS');
} catch (err) {
assert.equal((err as YError).code, 'E_BAD_AUTOLOADED_INITIALIZER');
assert.deepEqual((err as YError).params, ['test', undefined]);
}
});
assert.deepEqual(Object.keys(dependencies), ['time', 'hash']);
assert.deepEqual(dependencies, {
hash: { ENV: 'value_of:ENV', DEBUG: 'value_of:DEBUG' },
time: 'value_of:time',
});
});
test('should fail when autoloaded dependencies are not right initializers', async () => {
$.register(
initializer(
test('with lacking autoloaded dependencies', async () => {
const autoloaderInitializer = initializer(
{

@@ -924,34 +890,2 @@ type: 'service',

type: 'service',
name: 'not-' + serviceName,
inject: [],
},
async () => 'THE_TEST:' + serviceName,
),
}),
),
);
try {
await $.run<Record<string, any>>(['test']);
throw new YError('E_UNEXPECTED_SUCCESS');
} catch (err) {
assert.equal((err as YError).code, 'E_AUTOLOADED_INITIALIZER_MISMATCH');
assert.deepEqual((err as YError).params, ['test', 'not-test']);
}
});
test('should fail when autoload depends on existing autoloaded dependencies', async () => {
$.register(
initializer(
{
type: 'service',
name: '$autoload',
inject: ['ENV'],
singleton: true,
},
async () => async (serviceName) => ({
path: '/path/of/debug',
initializer: initializer(
{
type: 'service',
name: 'DEBUG',

@@ -963,82 +897,472 @@ inject: [],

}),
),
);
);
const wrappedProvider = provider(hashProvider, 'hash', [
'ENV',
'?DEBUG',
]);
try {
await $.run<Record<string, any>>(['test']);
throw new YError('E_UNEXPECTED_SUCCESS');
} catch (err) {
assert.equal((err as YError).code, 'E_AUTOLOADER_DYNAMIC_DEPENDENCY');
assert.deepEqual((err as YError).params, ['ENV']);
}
});
$.register(autoloaderInitializer);
$.register(wrappedProvider);
$.register(constant('ENV', ENV));
$.register(constant('time', time));
test('should work when autoload depends on optional and unexisting autoloaded dependencies', async () => {
$.register(
initializer(
{
type: 'service',
name: '$autoload',
inject: ['?ENV'],
singleton: true,
},
async () => async (serviceName) => ({
path: `/path/of/${serviceName}`,
initializer: initializer(
{
type: 'service',
name: serviceName,
inject: [],
},
async () => `THE_${serviceName.toUpperCase()}:` + serviceName,
),
}),
),
);
const dependencies = await $.run<any>(['time', 'hash']);
const dependencies = await $.run<Record<string, any>>(['test']);
assert.deepEqual(Object.keys(dependencies), ['time', 'hash']);
assert.deepEqual(dependencies, {
hash: { ENV, DEBUG: 'THE_DEBUG:DEBUG' },
time,
});
});
assert.deepEqual(Object.keys(dependencies), ['test']);
test('with deeper several lacking dependencies', async () => {
$.register(
initializer(
{
name: '$autoload',
type: 'service',
singleton: true,
},
async () => async (serviceName) => ({
path: `/path/to/${serviceName}`,
initializer: initializer(
{
type: 'provider',
name: serviceName,
inject:
'hash2' === serviceName
? ['hash1']
: 'hash4' === serviceName
? ['hash3']
: [],
},
hashProvider,
),
}),
),
);
$.register(constant('ENV', ENV));
$.register(constant('time', time));
$.register(provider(hashProvider, 'hash', ['ENV']));
$.register(provider(hashProvider, 'hash1', ['hash']));
$.register(provider(hashProvider, 'hash3', ['hash2']));
$.register(provider(hashProvider, 'hash5', ['hash4']));
const dependencies = await $.run<any>(['hash5', 'time']);
assert.deepEqual(Object.keys(dependencies), ['hash5', 'time']);
});
test('with various dependencies', async () => {
$.register(provider(hashProvider, 'hash', ['hash2']));
$.register(provider(hashProvider, 'hash3', ['?ENV']));
$.register(constant('DEBUG', 1));
$.register(
initializer(
{
type: 'service',
name: '$autoload',
inject: ['DEBUG'],
singleton: true,
},
async () => async (serviceName) => {
if ('ENV' === serviceName) {
throw new YError('E_UNMATCHED_DEPENDENCY');
}
return {
path: '/path/of/debug',
initializer: initializer(
{
type: 'service',
name: 'hash2',
inject: ['hash3'],
},
async () => 'THE_HASH:' + serviceName,
),
};
},
),
);
const dependencies = await $.run<any>(['hash', '?ENV']);
assert.deepEqual(Object.keys(dependencies), ['hash', 'ENV']);
});
test('and instanciate services once', async () => {
$.register(
initializer(
{
name: '$autoload',
type: 'service',
singleton: true,
},
async () => async (serviceName) => ({
path: `/path/to/${serviceName}`,
initializer: initializer(
{
type: 'provider',
name: serviceName,
inject: ['ENV', 'time'],
},
hashProvider,
),
}),
),
);
const timeServiceStub = sinon.spy(timeService);
$.register(constant('ENV', ENV));
$.register(service(timeServiceStub, 'time'));
$.register(provider(hashProvider, 'hash', ['hash1', 'hash2', 'hash3']));
$.register(
provider(hashProvider, 'hash_', ['hash1', 'hash2', 'hash3']),
);
const dependencies = await $.run<any>(['hash', 'hash_', 'hash3']);
assert.deepEqual(timeServiceStub.args, [[{}]]);
assert.deepEqual(Object.keys(dependencies), ['hash', 'hash_', 'hash3']);
});
test('with null service dependencies', async () => {
const time = jest.fn();
$.register(constant('time', time));
$.register(
initializer(
{
name: '$autoload',
type: 'service',
singleton: true,
},
async () => async (serviceName) => ({
path: `/path/to/${serviceName}`,
initializer: nullService,
}),
),
);
const dependencies = await $.run<any>(['nullService']);
assert.deepEqual(Object.keys(dependencies), ['nullService']);
assert.deepEqual(dependencies, {
nullService: null,
});
});
test('with null provider dependencies', async () => {
const time = jest.fn();
$.register(constant('time', time));
$.register(
initializer(
{
name: '$autoload',
type: 'service',
singleton: true,
},
async () => async (serviceName) => ({
path: `/path/to/${serviceName}`,
initializer: nullProvider,
}),
),
);
const dependencies = await $.run<any>(['nullProvider']);
assert.deepEqual(Object.keys(dependencies), ['nullProvider']);
assert.deepEqual(dependencies, {
nullProvider: null,
});
});
test('with undefined dependencies', async () => {
const time = jest.fn();
$.register(
initializer(
{
name: '$autoload',
type: 'service',
singleton: true,
},
async () => async (serviceName) => ({
path: `/path/to/${serviceName}`,
initializer:
serviceName === 'undefinedService'
? undefinedService
: undefinedProvider,
}),
),
);
$.register(constant('time', time));
const dependencies = await $.run<any>([
'undefinedService',
'undefinedProvider',
]);
assert.deepEqual(Object.keys(dependencies), [
'undefinedService',
'undefinedProvider',
]);
assert.deepEqual(dependencies, {
undefinedService: undefined,
undefinedProvider: null,
});
});
test('when autoload depends on optional and unexisting autoloaded dependencies', async () => {
$.register(
initializer(
{
type: 'service',
name: '$autoload',
inject: ['?ENV'],
singleton: true,
},
async () => async (serviceName) => ({
path: `/path/of/${serviceName}`,
initializer: initializer(
{
type: 'service',
name: serviceName,
inject: [],
},
async () => `THE_${serviceName.toUpperCase()}:` + serviceName,
),
}),
),
);
const dependencies = await $.run<any>(['test']);
assert.deepEqual(Object.keys(dependencies), ['test']);
});
test('when autoload depends on deeper optional and unexisting autoloaded dependencies', async () => {
$.register(
initializer(
{
type: 'service',
name: 'log',
inject: ['?LOG_ROUTING', '?LOGGER', '?debug'],
singleton: true,
},
async () => {
return () => undefined;
},
),
);
$.register(constant('LOGGER', 'LOGGER_CONSTANT'));
$.register(constant('debug', 'debug_value'));
$.register(
initializer(
{
type: 'service',
name: '$autoload',
inject: ['?ENV', '?log'],
singleton: true,
},
async () => async (serviceName) => ({
path: `/path/of/${serviceName}`,
initializer: initializer(
{
type: 'service',
name: serviceName,
inject: [],
},
async () => `THE_${serviceName.toUpperCase()}:` + serviceName,
),
}),
),
);
const dependencies = await $.run<any>(['test', 'log']);
assert.deepEqual(Object.keys(dependencies), ['test', 'log']);
});
});
test.skip('should work when autoload depends on deeper optional and unexisting autoloaded dependencies', async () => {
$.register(
initializer(
{
type: 'service',
name: 'log',
inject: ['?LOG_ROUTING', '?LOGGER', '?debug'],
},
async () => {
return () => undefined;
},
),
);
$.register(constant('LOGGER', 'LOGGER_CONSTANT'));
$.register(constant('debug', 'debug_value'));
$.register(
initializer(
{
type: 'service',
name: '$autoload',
inject: ['?ENV', '?log'],
singleton: true,
},
async () => async (serviceName) => ({
path: `/path/of/${serviceName}`,
initializer: initializer(
{
type: 'service',
name: serviceName,
inject: [],
},
async () => `THE_${serviceName.toUpperCase()}:` + serviceName,
),
}),
),
);
describe('should fail', () => {
test('when autoload does not exists', async () => {
try {
await $.run<any>(['test']);
throw new YError('E_UNEXPECTED_SUCCESS');
} catch (err) {
assert.equal((err as YError).code, 'E_UNMATCHED_DEPENDENCY');
}
});
const dependencies = await $.run<Record<string, any>>(['test', 'log']);
test('when autoloaded dependencies are not found', async () => {
$.register(
initializer(
{
type: 'service',
name: '$autoload',
inject: [],
singleton: true,
},
async () => async (serviceName) => {
throw new YError('E_CANNOT_AUTOLOAD', serviceName);
},
),
);
assert.deepEqual(Object.keys(dependencies), ['test', 'log']);
try {
await $.run<any>(['test']);
throw new YError('E_UNEXPECTED_SUCCESS');
} catch (err) {
assert.equal((err as YError).code, 'E_BAD_AUTOLOADED_INITIALIZER');
assert.deepEqual((err as YError).params, ['test']);
assert.equal(
((err as YError).wrappedErrors[0] as YError).code,
'E_CANNOT_AUTOLOAD',
);
assert.deepEqual(
((err as YError).wrappedErrors[0] as YError).params,
['test'],
);
}
});
test('when the autoloader returns bad data', async () => {
$.register(
initializer(
{
type: 'service',
name: '$autoload',
inject: [],
singleton: true,
},
async () => async () => 'not_an_initializer',
),
);
try {
await $.run<any>(['test']);
throw new YError('E_UNEXPECTED_SUCCESS');
} catch (err) {
assert.equal((err as YError).code, 'E_BAD_AUTOLOADED_INITIALIZER');
assert.deepEqual((err as YError).params, ['test']);
assert.equal(
((err as YError).wrappedErrors[0] as YError).code,
'E_BAD_AUTOLOADER_RESULT',
);
assert.deepEqual(
((err as YError).wrappedErrors[0] as YError).params,
['test', 'not_an_initializer'],
);
}
});
test('when autoloaded dependencies are not initializers', async () => {
$.register(
initializer(
{
type: 'service',
name: '$autoload',
inject: [],
singleton: true,
},
async () => async () => ({
initializer: 'not_an_initializer',
path: '/path/to/initializer',
}),
),
);
try {
await $.run<any>(['test']);
throw new YError('E_UNEXPECTED_SUCCESS');
} catch (err) {
assert.equal((err as YError).code, 'E_BAD_AUTOLOADED_INITIALIZER');
assert.deepEqual((err as YError).params, ['test']);
assert.equal(
((err as YError).wrappedErrors[0] as YError).code,
'E_AUTOLOADED_INITIALIZER_MISMATCH',
);
assert.deepEqual(
((err as YError).wrappedErrors[0] as YError).params,
['test', undefined],
);
}
});
test('when autoloaded dependencies are not right initializers', async () => {
$.register(
initializer(
{
type: 'service',
name: '$autoload',
inject: [],
singleton: true,
},
async () => async (serviceName) => ({
path: '/path/of/debug',
initializer: initializer(
{
type: 'service',
name: 'not-' + serviceName,
inject: [],
},
async () => 'THE_TEST:' + serviceName,
),
}),
),
);
try {
await $.run<any>(['test']);
throw new YError('E_UNEXPECTED_SUCCESS');
} catch (err) {
assert.equal((err as YError).code, 'E_BAD_AUTOLOADED_INITIALIZER');
assert.deepEqual((err as YError).params, ['test']);
assert.equal(
((err as YError).wrappedErrors[0] as YError).code,
'E_AUTOLOADED_INITIALIZER_MISMATCH',
);
assert.deepEqual(
((err as YError).wrappedErrors[0] as YError).params,
['test', 'not-test'],
);
}
});
test('when autoload depends on existing autoloaded dependencies', async () => {
$.register(
initializer(
{
type: 'service',
name: '$autoload',
inject: ['ENV'],
singleton: true,
},
async () => async (serviceName) => ({
path: '/path/of/debug',
initializer: initializer(
{
type: 'service',
name: 'DEBUG',
inject: [],
},
async () => 'THE_DEBUG:' + serviceName,
),
}),
),
);
try {
await $.run<any>(['test']);
throw new YError('E_UNEXPECTED_SUCCESS');
} catch (err) {
assert.equal((err as YError).code, 'E_UNMATCHED_DEPENDENCY');
assert.deepEqual((err as YError).params, ['__run__', 'test']);
}
});
});

@@ -1053,7 +1377,3 @@ });

const dependencies = await $.run<Record<string, any>>([
'time',
'hash',
'$injector',
]);
const dependencies = await $.run<any>(['time', 'hash', '$injector']);
assert.deepEqual(Object.keys(dependencies), [

@@ -1075,7 +1395,3 @@ 'time',

const dependencies = await $.run<Record<string, any>>([
'time',
'hash',
'$injector',
]);
const dependencies = await $.run<any>(['time', 'hash', '$injector']);
assert.deepEqual(Object.keys(dependencies), [

@@ -1100,7 +1416,3 @@ 'time',

const dependencies = await $.run<Record<string, any>>([
'time',
'hash',
'$injector',
]);
const dependencies = await $.run<any>(['time', 'hash', '$injector']);
assert.deepEqual(Object.keys(dependencies), [

@@ -1128,6 +1440,3 @@ 'time',

const dependencies = await $.run<Record<string, any>>([
'time',
'$injector',
]);
const dependencies = await $.run<any>(['time', '$injector']);
assert.deepEqual(Object.keys(dependencies), ['time', '$injector']);

@@ -1148,4 +1457,4 @@

const [{ hash }, { hash: sameHash }] = await Promise.all([
$.run<Record<string, any>>(['hash']),
$.run<Record<string, any>>(['hash']),
$.run<any>(['hash']),
$.run<any>(['hash']),
]);

@@ -1155,3 +1464,3 @@

const { hash: yaSameHash } = await $.run<Record<string, any>>(['hash']);
const { hash: yaSameHash } = await $.run<any>(['hash']);

@@ -1168,6 +1477,6 @@ assert.notEqual(hash, yaSameHash);

await Promise.all([
$.run<Record<string, any>>(['hash']),
$.run<Record<string, any>>(['hash']),
$.run<Record<string, any>>(['hash2']),
$.run<Record<string, any>>(['hash2']),
$.run<any>(['hash']),
$.run<any>(['hash']),
$.run<any>(['hash2']),
$.run<any>(['hash2']),
]);

@@ -1177,3 +1486,3 @@ assert.equal(hash, sameHash);

const { hash: yaSameHash } = await $.run<Record<string, any>>(['hash']);
const { hash: yaSameHash } = await $.run<any>(['hash']);

@@ -1187,3 +1496,3 @@ assert.equal(hash, yaSameHash);

assert.equal(typeof $.destroy, 'function');
const dependencies = await $.run<Record<string, any>>(['$instance']);
const dependencies = await $.run<any>(['$instance']);

@@ -1201,5 +1510,5 @@ await dependencies.$instance.destroy();

const [dependencies] = await Promise.all([
$.run<Record<string, any>>(['$instance']),
$.run<Record<string, any>>(['ENV', 'hash', 'hash1', 'time']),
$.run<Record<string, any>>(['ENV', 'hash', 'hash2']),
$.run<any>(['$instance']),
$.run<any>(['ENV', 'hash', 'hash1', 'time']),
$.run<any>(['ENV', 'hash', 'hash2']),
]);

@@ -1220,11 +1529,5 @@

const dependenciesBuckets = await Promise.all([
$.run<Record<string, any>>(['$instance']),
$.run<Record<string, any>>([
'$instance',
'ENV',
'hash',
'hash1',
'time',
]),
$.run<Record<string, any>>(['$instance', 'ENV', 'hash', 'hash2']),
$.run<any>(['$instance']),
$.run<any>(['$instance', 'ENV', 'hash', 'hash1', 'time']),
$.run<any>(['$instance', 'ENV', 'hash', 'hash2']),
]);

@@ -1247,12 +1550,7 @@

const [dependencies1, dependencies2] = await Promise.all([
$.run<Record<string, any>>(['$instance']),
$.run<Record<string, any>>([
'$dispose',
'ENV',
'hash',
'hash1',
'time',
]),
$.run<Record<string, any>>(['ENV', 'hash', 'hash2']),
$.run<any>(['$instance']),
$.run<any>(['$dispose', 'ENV', 'hash', 'hash1', 'time']),
$.run<any>(['ENV', 'hash', 'hash2']),
]);
await Promise.all([

@@ -1270,3 +1568,3 @@ dependencies2.$dispose(),

const dependencies = await $.run<Record<string, any>>(['$instance']);
const dependencies = await $.run<any>(['$instance']);

@@ -1278,3 +1576,3 @@ assert.equal(typeof dependencies.$instance.destroy, 'function');

try {
await $.run<Record<string, any>>(['ENV', 'hash', 'hash1']);
await $.run<any>(['ENV', 'hash', 'hash1']);
throw new YError('E_UNEXPECTED_SUCCES');

@@ -1289,3 +1587,3 @@ } catch (err) {

test('should work with no dependencies', async () => {
const dependencies = await $.run<Record<string, any>>(['$dispose']);
const dependencies = await $.run<any>(['$dispose']);
assert.equal(typeof dependencies.$dispose, 'function');

@@ -1300,7 +1598,3 @@

const dependencies = await $.run<Record<string, any>>([
'time',
'ENV',
'$dispose',
]);
const dependencies = await $.run<any>(['time', 'ENV', '$dispose']);
assert.deepEqual(Object.keys(dependencies), ['time', 'ENV', '$dispose']);

@@ -1316,7 +1610,3 @@

const dependencies = await $.run<Record<string, any>>([
'time',
'hash',
'$dispose',
]);
const dependencies = await $.run<any>(['time', 'hash', '$dispose']);
assert.deepEqual(Object.keys(dependencies), ['time', 'hash', '$dispose']);

@@ -1328,4 +1618,4 @@

test('should work with deeper dependencies', async () => {
let shutdownCallResolve;
let shutdownResolve;
let shutdownCallResolve: (value?: unknown) => void;
let shutdownResolve: (value?: unknown) => void;
const shutdownCallPromise = new Promise((resolve) => {

@@ -1364,3 +1654,3 @@ shutdownCallResolve = resolve;

const dependencies = await $.run<Record<string, any>>([
const dependencies = await $.run<any>([
'hash5',

@@ -1382,3 +1672,2 @@ 'time',

});
await dependencies.$dispose();

@@ -1389,4 +1678,4 @@ await finalPromise;

test('should work with deeper multi used dependencies', async () => {
let shutdownCallResolve;
let shutdownResolve;
let shutdownCallResolve: (value?: unknown) => void;
let shutdownResolve: (value?: unknown) => void;
const shutdownCallPromise = new Promise((resolve) => {

@@ -1421,3 +1710,3 @@ shutdownCallResolve = resolve;

const dependencies = await $.run<Record<string, any>>([
const dependencies = await $.run<any>([
'hash1',

@@ -1480,6 +1769,3 @@ 'hash2',

const dependencies = await $.run<Record<string, any>>([
'hash2',
'$dispose',
]);
const dependencies = await $.run<any>(['hash2', '$dispose']);
assert.deepEqual(Object.keys(dependencies), ['hash2', '$dispose']);

@@ -1500,8 +1786,4 @@ await dependencies.$dispose();

const { hash } = await $.run<Record<string, any>>(['time', 'hash']);
const dependencies = await $.run<Record<string, any>>([
'time',
'hash',
'$dispose',
]);
const { hash } = await $.run<any>(['time', 'hash']);
const dependencies = await $.run<any>(['time', 'hash', '$dispose']);

@@ -1512,6 +1794,3 @@ assert.equal(dependencies.hash, hash);

const newDependencies = await $.run<Record<string, any>>([
'time',
'hash',
]);
const newDependencies = await $.run<any>(['time', 'hash']);
assert.equal(newDependencies.hash, hash);

@@ -1525,11 +1804,7 @@ });

const { hash, $dispose } = await $.run<Record<string, any>>([
'time',
'hash',
'$dispose',
]);
const { hash, $dispose } = await $.run<any>(['time', 'hash', '$dispose']);
await $dispose();
const dependencies = await $.run<Record<string, any>>(['time', 'hash']);
const dependencies = await $.run<any>(['time', 'hash']);
assert.notEqual(dependencies.hash, hash);

@@ -1536,0 +1811,0 @@ });

@@ -0,1 +1,2 @@

/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint max-len: ["warn", { "ignoreComments": true }] @typescript-eslint/no-this-alias: "warn" */

@@ -100,2 +101,11 @@ import {

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 type KnifecycleOptions = {
sequential?: boolean;
};
export interface Injector<T extends Record<string, unknown>> {

@@ -112,9 +122,52 @@ (dependencies: DependencyDeclaration[]): Promise<T>;

}
export interface SiloContext<S> {
name: string;
servicesDescriptors: Map<DependencyDeclaration, Promise<Provider<S>>>;
servicesSequence: DependencyDeclaration[][];
servicesShutdownsPromises: Map<DependencyDeclaration, Promise<void>>;
export type SiloIndex = string;
export type BaseInitializerStateDescriptor<S, D extends Dependencies> = {
dependents: {
silo?: SiloIndex;
name: ServiceName;
optional: boolean;
}[];
initializerLoadPromise?: Promise<Initializer<S, D>>;
initializer?: Initializer<S, D>;
autoloaded: boolean;
};
export type SiloedInitializerStateDescriptor<
S,
D extends Dependencies,
> = BaseInitializerStateDescriptor<S, D> & {
silosInstances: Record<
SiloIndex,
{
dependency?: ServiceName;
provider?: NonNullable<Provider<S> | typeof NO_PROVIDER>;
providerLoadPromise?: Promise<void>;
instanceDisposePromise?: Promise<S>;
}
>;
};
export type SingletonInitializerStateDescriptor<
S,
D extends Dependencies,
> = BaseInitializerStateDescriptor<S, D> & {
singletonProvider?: NonNullable<Provider<S> | typeof NO_PROVIDER>;
singletonProviderLoadPromise?: Promise<void>;
disposer?: Disposer;
fatalErrorPromise?: FatalErrorPromise;
};
export type AutoloadedInitializerStateDescriptor<
S,
D extends Dependencies,
> = BaseInitializerStateDescriptor<S, D> & {
autoloaded: true;
};
export type InitializerStateDescriptor<S, D extends Dependencies> =
| SingletonInitializerStateDescriptor<S, D>
| SiloedInitializerStateDescriptor<S, D>
| AutoloadedInitializerStateDescriptor<S, D>;
export interface SiloContext {
index: SiloIndex;
loadingServices: ServiceName[];
loadingSequences: ServiceName[][];
errorsPromises: Promise<void>[];
shutdownPromise?: Promise<void>;
_shutdownPromise?: Promise<void>;
throwFatalError?: (err: Error) => void;

@@ -131,3 +184,3 @@ }

$instance: Knifecycle;
$siloContext: SiloContext<unknown>;
$siloContext: SiloContext;
$fatalError: FatalErrorService;

@@ -145,15 +198,2 @@ };

const E_BAD_AUTOLOADED_INITIALIZER = 'E_BAD_AUTOLOADED_INITIALIZER';
const E_AUTOLOADED_INITIALIZER_MISMATCH = 'E_AUTOLOADED_INITIALIZER_MISMATCH';
const E_UNMATCHED_DEPENDENCY = 'E_UNMATCHED_DEPENDENCY';
const E_CIRCULAR_DEPENDENCY = 'E_CIRCULAR_DEPENDENCY';
const E_BAD_SERVICE_PROVIDER = 'E_BAD_SERVICE_PROVIDER';
const E_BAD_SERVICE_PROMISE = 'E_BAD_SERVICE_PROMISE';
const E_INSTANCE_DESTROYED = 'E_INSTANCE_DESTROYED';
const E_AUTOLOADER_DYNAMIC_DEPENDENCY = 'E_AUTOLOADER_DYNAMIC_DEPENDENCY';
const E_BAD_CLASS = 'E_BAD_CLASS';
const E_UNDEFINED_CONSTANT_INITIALIZER = 'E_UNDEFINED_CONSTANT_INITIALIZER';
const E_BAD_VALUED_NON_CONSTANT_INITIALIZER =
'E_BAD_VALUED_NON_CONSTANT_INITIALIZER';
/* Architecture Note #1: Knifecycle

@@ -175,2 +215,5 @@

set as a property.
In fact, the Knifecycle API is aimed to allow to statically
build its services load/unload code once in production.
*/

@@ -189,25 +232,17 @@

class Knifecycle {
private _options: KnifecycleOptions;
private _silosCounter: number;
private _silosContexts: Set<SiloContext<unknown>>;
private _initializers: Map<
private _silosContexts: Record<SiloIndex, SiloContext>;
private _initializersStates: Record<
string,
ProviderInitializer<Record<string, unknown>, unknown>
InitializerStateDescriptor<unknown, Dependencies>
>;
private _initializerResolvers: Map<
string,
Promise<ProviderInitializer<Record<string, unknown>, unknown>>
>;
private _singletonsServicesHandles: Map<string, Set<string>>;
private _singletonsServicesDescriptors: Map<
string,
{
promise: Promise<Provider<unknown>>;
preloaded: boolean;
}
>;
private _singletonsServicesShutdownsPromises: Map<string, Promise<void>>;
private shutdownPromise?: Promise<void>;
private _shutdownPromise?: Promise<void>;
/**
* Create a new Knifecycle instance
* @param {Object} options
* An object with options
* @param {boolean} options.sequential
* Allows to load dependencies sequentially (usefull for debugging)
* @return {Knifecycle}

@@ -221,27 +256,53 @@ * The Knifecycle instance

*/
constructor() {
constructor(options?: KnifecycleOptions) {
this._options = options || {};
this._silosCounter = 0;
this._silosContexts = new Set();
this._initializers = new Map();
this._initializerResolvers = new Map();
this._singletonsServicesHandles = new Map();
this._singletonsServicesDescriptors = new Map();
this._singletonsServicesShutdownsPromises = new Map();
this._silosContexts = {};
this._initializersStates = {
[FATAL_ERROR]: {
initializer: service(async () => {
throw new YError('E_UNEXPECTED_INIT', FATAL_ERROR);
}, FATAL_ERROR),
autoloaded: false,
dependents: [],
silosInstances: {},
},
[SILO_CONTEXT]: {
initializer: service(async () => {
throw new YError('E_UNEXPECTED_INIT', SILO_CONTEXT);
}, SILO_CONTEXT),
autoloaded: false,
dependents: [],
silosInstances: {},
},
[DISPOSE]: {
initializer: service(async () => {
throw new YError('E_UNEXPECTED_INIT', DISPOSE);
}, DISPOSE),
autoloaded: false,
dependents: [],
silosInstances: {},
},
};
this.register(constant(INSTANCE, this));
const initInjectorProvider = provider(
async ({ $siloContext }: { $siloContext: SiloContext<unknown> }) => ({
service: async (dependenciesDeclarations: DependencyDeclaration[]) =>
_buildFinalHash(
await this._initializeDependencies(
$siloContext,
$siloContext.name,
dependenciesDeclarations,
{ injectorContext: true, autoloading: false },
),
async ({
$siloContext,
$instance,
}: {
$siloContext: SiloContext;
$instance: Knifecycle;
}) => ({
service: async (dependenciesDeclarations: DependencyDeclaration[]) => {
return $instance._loadInitializerDependencies(
$siloContext,
[INJECTOR_DEPENDENT_NAME],
dependenciesDeclarations,
),
[],
);
},
}),
INJECTOR,
[SILO_CONTEXT],
[SILO_CONTEXT, INSTANCE],
// Despite its global definition, the injector

@@ -264,3 +325,3 @@ // depends on the silo context and then needs

- constants: a `constant` initializer resolves to
a constant value.
any constant value.
- services: a `service` initializer directly

@@ -274,8 +335,9 @@ resolve to the actual service it builds. It can

`fatalErrorPromise` that will be rejected if an
unrecoverable error happens.
unrecoverable error happens allowing Knifecycle
to terminate.
Initializers can be declared as singletons. This means
that they will be instanciated once for all for each
executions silos using them (we will cover this
topic later on).
Initializers can be declared as singletons (constants are
of course only singletons). This means that they will be
instanciated once for all for each executions silos using
them (we will cover this topic later on).
*/

@@ -291,62 +353,150 @@

register<T extends Initializer<unknown, any>>(initializer: T): Knifecycle {
if (this.shutdownPromise) {
throw new YError(E_INSTANCE_DESTROYED);
if (this._shutdownPromise) {
throw new YError('E_INSTANCE_DESTROYED');
}
unwrapInitializerProperties(initializer);
const initializerState: InitializerStateDescriptor<any, any> = {
initializer,
autoloaded: false,
dependents: [],
};
// Temporary cast constants into providers
// Best would be to threat each differently
// at dependencies initialization level to boost performances
if (initializer[SPECIAL_PROPS.TYPE] === 'constant') {
const value = initializer[SPECIAL_PROPS.VALUE];
this._checkInitializerOverride(initializer[SPECIAL_PROPS.NAME]);
if ('undefined' === typeof value) {
throw new YError(
E_UNDEFINED_CONSTANT_INITIALIZER,
initializer[SPECIAL_PROPS.NAME],
);
}
this._buildInitializerState(initializerState, initializer);
initializer = provider(
async () => ({
service: value,
}),
initializer[SPECIAL_PROPS.NAME],
[],
true,
) as T;
this._initializersStates[initializer[SPECIAL_PROPS.NAME]] =
initializerState;
// Needed for the build utils to still recognize
// this initializer as a constant value
initializer[SPECIAL_PROPS.VALUE] = value;
initializer[SPECIAL_PROPS.TYPE] = 'constant';
} else if ('undefined' !== typeof initializer[SPECIAL_PROPS.VALUE]) {
throw new YError(
E_BAD_VALUED_NON_CONSTANT_INITIALIZER,
initializer[SPECIAL_PROPS.NAME],
);
debug(`Registered an initializer: ${initializer[SPECIAL_PROPS.NAME]}`);
return this;
}
_checkInitializerOverride(serviceName: ServiceName) {
if (this._initializersStates[serviceName]) {
if ('initializer' in this._initializersStates[serviceName]) {
if (this._initializersStates[serviceName]?.dependents?.length) {
debug(
`Override attempt of an already used initializer: ${serviceName}`,
);
throw new YError('E_INITIALIZER_ALREADY_INSTANCIATED', serviceName);
}
debug(`Overridden an initializer: ${serviceName}`);
}
}
}
// Temporary cast service initializers into
// providers. Best would be to threat each differently
// at dependencies initialization level to boost performances
if ('service' === initializer[SPECIAL_PROPS.TYPE]) {
initializer = reuseSpecialProps(
initializer,
serviceAdapter.bind(
null,
initializer[SPECIAL_PROPS.NAME] as string,
initializer as ServiceInitializer<Record<string, unknown>, unknown>,
),
) as T;
initializer[SPECIAL_PROPS.TYPE] = 'provider';
_buildInitializerState(
initializerState: InitializerStateDescriptor<any, any>,
initializer: Initializer<unknown, any>,
): void {
unwrapInitializerProperties(initializer);
if (initializer[SPECIAL_PROPS.TYPE] === 'constant') {
const provider = {
service: initializer[SPECIAL_PROPS.VALUE],
};
(
initializerState as SingletonInitializerStateDescriptor<any, any>
).singletonProvider = provider;
(
initializerState as SingletonInitializerStateDescriptor<any, any>
).singletonProviderLoadPromise = Promise.resolve();
} else {
this._checkInitializerDependencies(initializer);
if (!initializer[SPECIAL_PROPS.SINGLETON]) {
(
initializerState as SiloedInitializerStateDescriptor<any, any>
).silosInstances = {};
}
}
}
_checkInitializerDependencies(initializer: Initializer<any, any>) {
const initializerDependsOfItself = initializer[SPECIAL_PROPS.INJECT]
.map(_pickServiceNameFromDeclaration)
.map((dependencyDeclaration) => {
const serviceName = _pickServiceNameFromDeclaration(
dependencyDeclaration,
);
if (
// TEMPFIX: let's build
initializer[SPECIAL_PROPS.NAME] !== 'BUILD_CONSTANTS' &&
// TEMPFIX: Those services are special...
![FATAL_ERROR, INJECTOR, SILO_CONTEXT].includes(serviceName) &&
initializer[SPECIAL_PROPS.SINGLETON] &&
this._initializersStates[serviceName] &&
'initializer' in this._initializersStates[serviceName] &&
this._initializersStates[serviceName]?.initializer &&
!this._initializersStates[serviceName]?.initializer?.[
SPECIAL_PROPS.SINGLETON
]
) {
debug(
`Found an inconsistent singleton initializer dependency: ${
initializer[SPECIAL_PROPS.NAME]
}`,
serviceName,
initializer,
);
throw new YError(
'E_BAD_SINGLETON_DEPENDENCIES',
initializer[SPECIAL_PROPS.NAME],
serviceName,
);
}
return serviceName;
})
.includes(initializer[SPECIAL_PROPS.NAME]);
if (
// TEMPFIX: let's build
initializer[SPECIAL_PROPS.NAME] !== 'BUILD_CONSTANTS' &&
!initializer[SPECIAL_PROPS.SINGLETON]
) {
Object.keys(this._initializersStates)
.filter(
(serviceName) =>
![
// TEMPFIX: Those services are special...
FATAL_ERROR,
INJECTOR,
SILO_CONTEXT,
].includes(serviceName),
)
.forEach((serviceName) => {
if (
this._initializersStates[serviceName]?.initializer &&
this._initializersStates[serviceName]?.initializer?.[
SPECIAL_PROPS.SINGLETON
] &&
(
this._initializersStates[serviceName]?.initializer?.[
SPECIAL_PROPS.INJECT
] || []
)
.map(_pickServiceNameFromDeclaration)
.includes(initializer[SPECIAL_PROPS.NAME])
) {
debug(
`Found an inconsistent dependent initializer: ${
initializer[SPECIAL_PROPS.NAME]
}`,
serviceName,
initializer,
);
throw new YError(
'E_BAD_SINGLETON_DEPENDENCIES',
serviceName,
initializer[SPECIAL_PROPS.NAME],
);
}
});
}
if (initializerDependsOfItself) {
throw new YError(E_CIRCULAR_DEPENDENCY, initializer[SPECIAL_PROPS.NAME]);
throw new YError(
'E_CIRCULAR_DEPENDENCY',
initializer[SPECIAL_PROPS.NAME],
);
}

@@ -360,54 +510,2 @@

});
if (this._initializers.has(initializer[SPECIAL_PROPS.NAME])) {
const initializedAsSingleton =
this._singletonsServicesHandles.has(initializer[SPECIAL_PROPS.NAME]) &&
this._singletonsServicesDescriptors.has(
initializer[SPECIAL_PROPS.NAME],
) &&
!this._singletonsServicesDescriptors.get(
initializer[SPECIAL_PROPS.NAME],
)?.preloaded;
const initializedAsInstance = [...this._silosContexts.values()].some(
(siloContext) =>
siloContext.servicesSequence.some((sequence) =>
sequence.includes(initializer[SPECIAL_PROPS.NAME]),
),
);
if (initializedAsSingleton || initializedAsInstance) {
throw new YError(
'E_INITIALIZER_ALREADY_INSTANCIATED',
initializer[SPECIAL_PROPS.NAME],
);
}
debug(`Overridden an initializer: ${initializer[SPECIAL_PROPS.NAME]}`);
} else {
debug(`Registered an initializer: ${initializer[SPECIAL_PROPS.NAME]}`);
}
// Constants are singletons and constant so we can set it
// to singleton services descriptors map directly
if ('constant' === initializer[SPECIAL_PROPS.TYPE]) {
const handlesSet = new Set<string>();
this._singletonsServicesHandles.set(
initializer[SPECIAL_PROPS.NAME],
handlesSet,
);
this._singletonsServicesDescriptors.set(initializer[SPECIAL_PROPS.NAME], {
preloaded: true,
// We do not directly use initializer[SPECIAL_PROPS.VALUE] here
// since it looks like there is a bug with Babel build that
// change functions to empty litteral objects
promise: (
initializer as ProviderInitializer<Record<string, unknown>, unknown>
)({}),
});
}
this._initializers.set(
initializer[SPECIAL_PROPS.NAME],
initializer as ProviderInitializer<Record<string, unknown>, unknown>,
);
return this;
}

@@ -421,9 +519,10 @@

const serviceName = _pickServiceNameFromDeclaration(dependencyDeclaration);
const dependencyProvider = this._initializers.get(serviceName);
const initializersState = this._initializersStates[serviceName];
if (!dependencyProvider) {
if (!initializersState || !initializersState.initializer) {
return;
}
declarationsStacks = declarationsStacks.concat(dependencyDeclaration);
dependencyProvider[SPECIAL_PROPS.INJECT].forEach(
(initializersState.initializer[SPECIAL_PROPS.INJECT] || []).forEach(
(childDependencyDeclaration) => {

@@ -436,3 +535,3 @@ const childServiceName = _pickServiceNameFromDeclaration(

throw new YError(
E_CIRCULAR_DEPENDENCY,
'E_CIRCULAR_DEPENDENCY',
...[rootServiceName]

@@ -499,21 +598,25 @@ .concat(declarationsStacks)

): string {
const servicesProviders = this._initializers;
const links: MermaidLink[] = Array.from(servicesProviders.keys())
const initializersStates = this._initializersStates;
const links: MermaidLink[] = Object.keys(initializersStates)
.filter((provider) => !provider.startsWith('$'))
.reduce((links, serviceName) => {
const serviceProvider = servicesProviders.get(
serviceName,
) as ProviderInitializer<Record<string, unknown>, unknown>;
const initializerState = initializersStates[serviceName];
if (!serviceProvider[SPECIAL_PROPS.INJECT].length) {
if (
!initializerState ||
!initializerState.initializer ||
!initializerState.initializer[SPECIAL_PROPS.INJECT]?.length
) {
return links;
}
return links.concat(
serviceProvider[SPECIAL_PROPS.INJECT].map((dependencyDeclaration) => {
const dependedServiceName = _pickServiceNameFromDeclaration(
dependencyDeclaration,
);
initializerState.initializer[SPECIAL_PROPS.INJECT].map(
(dependencyDeclaration) => {
const dependedServiceName = _pickServiceNameFromDeclaration(
dependencyDeclaration,
);
return { serviceName, dependedServiceName };
}),
return { serviceName, dependedServiceName };
},
),
);

@@ -583,22 +686,22 @@ }, []);

): Promise<ID> {
const _this = this;
const internalDependencies = [
...new Set(dependenciesDeclarations.concat(DISPOSE)),
];
const siloContext: SiloContext<unknown> = {
name: `silo-${this._silosCounter++}`,
servicesDescriptors: new Map(),
servicesSequence: [],
servicesShutdownsPromises: new Map(),
const siloIndex: SiloIndex = `silo-${this._silosCounter++}`;
const siloContext: SiloContext = {
index: siloIndex,
loadingServices: [],
loadingSequences: [],
errorsPromises: [],
};
if (this.shutdownPromise) {
throw new YError(E_INSTANCE_DESTROYED);
if (this._shutdownPromise) {
throw new YError('E_INSTANCE_DESTROYED');
}
// Create a provider for the special fatal error service
siloContext.servicesDescriptors.set(
FATAL_ERROR,
Promise.resolve({
(
this._initializersStates[FATAL_ERROR] as SiloedInitializerStateDescriptor<
FatalErrorService,
Dependencies<unknown>
>
).silosInstances[siloIndex] = {
provider: {
service: {

@@ -612,554 +715,719 @@ promise: new Promise<void>((_resolve, reject) => {

},
}),
);
},
};
// Make the siloContext available for internal injections
siloContext.servicesDescriptors.set(
SILO_CONTEXT,
Promise.resolve({
service: siloContext,
}),
);
(
this._initializersStates[
SILO_CONTEXT
] as SiloedInitializerStateDescriptor<SiloContext, Dependencies<unknown>>
).silosInstances[siloIndex] = {
provider: { service: siloContext },
};
// Create a provider for the shutdown special dependency
siloContext.servicesDescriptors.set(
DISPOSE,
Promise.resolve({
(
this._initializersStates[DISPOSE] as SiloedInitializerStateDescriptor<
Disposer,
Dependencies<unknown>
>
).silosInstances[siloIndex] = {
provider: {
service: async () => {
siloContext.shutdownPromise =
siloContext.shutdownPromise ||
_shutdownNextServices(siloContext.servicesSequence);
const _this = this;
siloContext._shutdownPromise =
siloContext._shutdownPromise ||
_shutdownNextServices(siloContext.loadingSequences.concat());
await siloContext._shutdownPromise;
delete this._silosContexts[siloContext.index];
debug('Shutting down services');
await siloContext.shutdownPromise;
this._silosContexts.delete(siloContext);
// Shutdown services in their instanciation order
async function _shutdownNextServices(reversedServiceSequence) {
if (0 === reversedServiceSequence.length) {
async function _shutdownNextServices(
serviceLoadSequences: ServiceName[][],
) {
if (0 === serviceLoadSequences.length) {
return;
}
const currentServiceLoadSequence = serviceLoadSequences.pop() || [];
await Promise.all(
reversedServiceSequence.pop().map(async (serviceName) => {
const singletonServiceDescriptor =
await _this._pickupSingletonServiceDescriptorPromise(
serviceName,
);
const serviceDescriptor =
singletonServiceDescriptor ||
(await siloContext.servicesDescriptors.get(serviceName));
let serviceShutdownPromise: Promise<void> | undefined =
_this._singletonsServicesShutdownsPromises.get(serviceName) ||
siloContext.servicesShutdownsPromises.get(serviceName);
if (serviceShutdownPromise) {
debug('Reusing a service shutdown promise:', serviceName);
return serviceShutdownPromise;
}
// 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 (
reversedServiceSequence.some((servicesDeclarations) =>
servicesDeclarations.includes(serviceName),
)
currentServiceLoadSequence
.filter(
(anotherServiceName) =>
anotherServiceName !== serviceName,
)
.some((anotherServiceName) =>
(
_this._initializersStates[anotherServiceName]
?.initializer?.[SPECIAL_PROPS.INJECT] || []
)
.map(_pickServiceNameFromDeclaration)
.includes(serviceName),
)
) {
debug('Delaying service shutdown:', serviceName);
return Promise.resolve();
debug(
`Delaying service "${serviceName}" dependencies shutdown to a dedicated batch.'`,
);
return true;
}
if (singletonServiceDescriptor) {
const handleSet = _this._singletonsServicesHandles.get(
serviceName,
) as Set<string>;
});
handleSet.delete(siloContext.name);
if (handleSet.size) {
debug(
'Singleton is used elsewhere:',
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,
handleSet,
);
return Promise.resolve();
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();
}
}
_this._singletonsServicesDescriptors.delete(serviceName);
}
debug('Shutting down a service:', serviceName);
serviceShutdownPromise = serviceDescriptor?.dispose
? serviceDescriptor.dispose()
: Promise.resolve();
if (singletonServiceDescriptor) {
_this._singletonsServicesShutdownsPromises.set(
serviceName,
serviceShutdownPromise,
);
}
siloContext.servicesShutdownsPromises.set(
serviceName,
serviceShutdownPromise,
);
return serviceShutdownPromise;
}),
}),
);
await _shutdownNextServices(reversedServiceSequence);
if (dependendedByAServiceInTheSameBatch.length) {
serviceLoadSequences.unshift(dependendedByAServiceInTheSameBatch);
}
await _shutdownNextServices(serviceLoadSequences);
}
},
dispose: Promise.resolve.bind(Promise),
}),
);
},
};
this._silosContexts[siloContext.index] = siloContext;
this._silosContexts.add(siloContext);
const servicesHash = await this._initializeDependencies(
const services = await this._loadInitializerDependencies(
siloContext,
siloContext.name,
internalDependencies,
{ injectorContext: false, autoloading: false },
[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);
return _buildFinalHash(
servicesHash,
dependenciesDeclarations,
) as unknown as ID;
debug('All dependencies now loaded:', siloContext.loadingSequences);
return services as ID;
}
/**
* Destroy the Knifecycle instance
* @return {Promise}
* Full destruction promise
* @example
*
* import Knifecycle, { constant } from 'knifecycle'
*
* const $ = new Knifecycle();
*
* $.register(constant('ENV', process.env));
* $.run(['ENV'])
* .then(({ ENV }) => {
* // Here goes your code
*
* // Finally destroy the instance
* $.destroy()
* })
*/
async destroy(): Promise<void> {
this.shutdownPromise =
this.shutdownPromise ||
Promise.all(
[...this._silosContexts].map(async (siloContext) => {
const $dispose = (await siloContext.servicesDescriptors.get(DISPOSE))
?.service as Disposer;
return $dispose();
}),
).then(() => undefined);
debug('Shutting down Knifecycle instance.');
return this.shutdownPromise;
_getInitializer(
serviceName: ServiceName,
): Initializer<unknown, Dependencies> | undefined {
return this._initializersStates[serviceName]?.initializer;
}
/**
* Initialize or return a service descriptor
* @param {Object} siloContext
* Current execution silo context
* @param {String} serviceName
* Service name.
* @param {Object} options
* Options for service retrieval
* @param {Boolean} options.injectorContext
* Flag indicating the injection were initiated by the $injector
* @param {Boolean} options.autoloading
* Flag to indicating $autoload dependencies on the fly loading
* @param {String} serviceProvider
* Service provider.
* @return {Promise}
* Service descriptor promise.
*/
async _getServiceDescriptor(
siloContext: SiloContext<unknown>,
_getServiceProvider(
siloContext: SiloContext,
serviceName: ServiceName,
{
injectorContext,
autoloading,
}: { injectorContext: boolean; autoloading: boolean },
): Promise<Provider<unknown>> {
// Try to get service descriptior early from the silo context
let serviceDescriptorPromise =
siloContext.servicesDescriptors.get(serviceName);
if (serviceDescriptorPromise) {
if (autoloading) {
debug(
`⚠️ - Possible dead lock due to reusing "${serviceName}" from the silo context while autoloading.`,
);
}
return serviceDescriptorPromise;
): Provider<unknown> | typeof NO_PROVIDER | undefined {
const initializerState = this._initializersStates[serviceName];
// This method expect the initialized to have a state
// so failing early if not to avoid programming errors
if (!initializerState) {
throw new YError('E_UNEXPECTED_SERVICE_READ');
}
if ('initializer' in initializerState) {
if ('singletonProvider' in initializerState) {
const provider = initializerState.singletonProvider;
const initializer = await this._findInitializer(siloContext, serviceName, {
injectorContext,
autoloading,
});
if (provider) {
return provider;
}
}
serviceDescriptorPromise = this._pickupSingletonServiceDescriptorPromise(
serviceName,
) as Promise<Provider<unknown>>;
if (
'silosInstances' in initializerState &&
initializerState.silosInstances &&
initializerState.silosInstances[siloContext.index] &&
'provider' in initializerState.silosInstances[siloContext.index]
) {
const provider =
initializerState.silosInstances[siloContext.index].provider;
if (serviceDescriptorPromise as Promise<Provider<unknown>> | undefined) {
if (autoloading) {
debug(
`⚠️ - Possible dead lock due to reusing the singleton "${serviceName}" while autoloading.`,
);
if (provider) {
return provider;
}
}
(this._singletonsServicesHandles.get(serviceName) as Set<string>).add(
siloContext.name,
);
} else {
serviceDescriptorPromise =
siloContext.servicesDescriptors.get(serviceName);
}
if (serviceDescriptorPromise) {
return serviceDescriptorPromise;
}
return;
}
// The $injector service is mainly intended to be used as a workaround
// for unavoidable circular dependencies. It rarely make sense to
// instanciate new services at this level so printing a warning for
// debug purposes
if (injectorContext) {
debug(
'Warning: Instantiating a new service via the $injector. It may' +
' mean that you no longer need it if your worked around a circular' +
' dependency.',
);
}
serviceDescriptorPromise = this._initializeServiceDescriptor(
siloContext,
serviceName,
initializer,
{
autoloading: autoloading || AUTOLOAD === serviceName,
injectorContext,
},
async _loadInitializerDependencies(
siloContext: SiloContext,
parentsNames: ServiceName[],
dependenciesDeclarations: DependencyDeclaration[],
additionalDeclarations: DependencyDeclaration[],
): Promise<Dependencies> {
debug(
`${[...parentsNames].join(
'->',
)}: Gathering the dependencies (${dependenciesDeclarations.join(', ')}).`,
);
const allDependenciesDeclarations = [
...new Set(dependenciesDeclarations.concat(additionalDeclarations)),
];
const dependencies: ServiceName[] = [];
const lackingDependencies: ServiceName[] = [];
if (initializer[SPECIAL_PROPS.SINGLETON]) {
const handlesSet = new Set<string>();
for (const serviceDeclaration of allDependenciesDeclarations) {
const { mappedName, optional } =
parseDependencyDeclaration(serviceDeclaration);
const initializerState = this._initializersStates[mappedName] || {
dependents: [],
autoloaded: true,
};
handlesSet.add(siloContext.name);
this._singletonsServicesHandles.set(serviceName, handlesSet);
this._singletonsServicesDescriptors.set(serviceName, {
preloaded: false,
promise: serviceDescriptorPromise,
this._initializersStates[mappedName] = initializerState;
initializerState.dependents.push({
silo: siloContext.index,
name: parentsNames[parentsNames.length - 1],
optional,
});
} else {
siloContext.servicesDescriptors.set(
serviceName,
serviceDescriptorPromise,
);
}
// Since the autoloader is a bit special it must be pushed here
if (AUTOLOAD === serviceName) {
siloContext.servicesSequence.unshift([AUTOLOAD]);
}
return serviceDescriptorPromise;
}
async _findInitializer(
siloContext: SiloContext<unknown>,
serviceName: ServiceName,
{
injectorContext,
autoloading,
}: { injectorContext: boolean; autoloading: boolean },
): Promise<ProviderInitializer<Record<string, unknown>, unknown>> {
const initializer = this._initializers.get(serviceName);
if (initializer) {
return initializer;
dependencies.push(mappedName);
}
// The auto loader must only have static dependencies
// and we have to do this check here to avoid caching
// non-autoloading request and then be blocked by an
// autoloader dep that waits for that cached load
if (autoloading) {
throw new YError(E_AUTOLOADER_DYNAMIC_DEPENDENCY, serviceName);
}
do {
const previouslyLackingDependencies = [...lackingDependencies];
debug('No service provider:', serviceName);
lackingDependencies.length = 0;
let initializerPromise = this._initializerResolvers.get(serviceName);
for (const mappedName of dependencies) {
if (!this._getServiceProvider(siloContext, mappedName)) {
lackingDependencies.push(mappedName);
if (!siloContext.loadingServices.includes(mappedName)) {
siloContext.loadingServices.push(mappedName);
}
}
}
if (initializerPromise) {
return await initializerPromise;
}
initializerPromise = (async () => {
if (!this._initializers.get(AUTOLOAD)) {
throw new YError(E_UNMATCHED_DEPENDENCY, serviceName);
}
debug(`Loading the $autoload service to lookup for: ${serviceName}.`);
try {
const autoloadingDescriptor = (await this._getServiceDescriptor(
if (lackingDependencies.length) {
await this._resolveDependencies(
siloContext,
AUTOLOAD,
{ injectorContext, autoloading: true },
)) as Provider<
Autoloader<Initializer<unknown, Record<string, unknown>>>
>;
const { initializer, path } = await autoloadingDescriptor.service(
serviceName,
lackingDependencies,
parentsNames,
);
}
const loadSequence = previouslyLackingDependencies.filter(
(previouslyLackingDependency) =>
!lackingDependencies.includes(previouslyLackingDependency),
);
if (
typeof initializer !== 'function' &&
(typeof initializer !== 'object' ||
initializer[SPECIAL_PROPS.TYPE] !== 'constant')
) {
if (loadSequence.length) {
siloContext.loadingSequences.push(loadSequence);
}
} while (lackingDependencies.length);
return dependenciesDeclarations.reduce(
(finalHash, dependencyDeclaration) => {
const { serviceName, mappedName, optional } =
parseDependencyDeclaration(dependencyDeclaration);
const provider = this._getServiceProvider(siloContext, mappedName);
// We expect a provider here since everything
// should be resolved
if (!provider) {
throw new YError(
E_BAD_AUTOLOADED_INITIALIZER,
'E_UNEXPECTED_PROVIDER_STATE',
serviceName,
initializer,
parentsNames,
);
}
if (initializer[SPECIAL_PROPS.NAME] !== serviceName) {
if (!optional && provider === NO_PROVIDER) {
throw new YError(
E_AUTOLOADED_INITIALIZER_MISMATCH,
'E_UNMATCHED_DEPENDENCY',
...parentsNames,
serviceName,
initializer[SPECIAL_PROPS.NAME],
);
}
debug(`Loaded the ${serviceName} initializer at path ${path}.`);
this.register(initializer);
this._initializerResolvers.delete(serviceName);
// Here we need to pick-up the registered initializer to
// have a universally usable intitializer
return this._initializers.get(serviceName);
} catch (err) {
debug(`Could not load ${serviceName} via the auto loader.`);
throw err;
}
})() as Promise<ProviderInitializer<Record<string, unknown>, unknown>>;
if (provider === NO_PROVIDER) {
debug(
`${[...parentsNames, serviceName].join(
'->',
)}: Optional dependency not found.`,
);
}
this._initializerResolvers.set(
serviceName,
initializerPromise as Promise<
ProviderInitializer<Record<string, unknown>, unknown>
>,
finalHash[serviceName] = (provider as Provider<unknown>).service;
return finalHash;
},
{},
);
return await (initializerPromise as Promise<
ProviderInitializer<Record<string, unknown>, unknown>
>);
}
_pickupSingletonServiceDescriptorPromise(
async _loadProvider(
siloContext: SiloContext,
serviceName: ServiceName,
): Promise<Provider<unknown>> | void {
const serviceDescriptor =
this._singletonsServicesDescriptors.get(serviceName);
parentsNames: ServiceName[],
): Promise<void> {
debug(
`${[...parentsNames, serviceName].join('->')}: Loading the provider...`,
);
if (!serviceDescriptor) {
const initializerState = this._initializersStates[serviceName];
if (!('initializer' in initializerState) || !initializerState.initializer) {
// At that point there should be an initialiser property
throw new YError('E_UNEXPECTED_INITIALIZER_STATE', serviceName);
}
const services = await this._loadInitializerDependencies(
siloContext,
[...parentsNames, serviceName],
initializerState.initializer[SPECIAL_PROPS.INJECT],
[],
);
let providerPromise: Promise<Provider<unknown>>;
if (initializerState.initializer[SPECIAL_PROPS.TYPE] === 'service') {
const servicePromise = (
initializerState.initializer as ServiceInitializer<
Dependencies,
Service
>
)(services);
if (!servicePromise || !servicePromise.then) {
debug('Service initializer did not return a promise:', serviceName);
throw new YError('E_BAD_SERVICE_PROMISE', serviceName);
}
providerPromise = servicePromise.then((service) => ({ service }));
} else if (
initializerState.initializer[SPECIAL_PROPS.TYPE] === 'provider'
) {
providerPromise = (
initializerState.initializer as ProviderInitializer<
Dependencies,
Service
>
)(services);
if (!providerPromise || !providerPromise.then) {
debug('Provider initializer did not return a promise:', serviceName);
throw new YError('E_BAD_SERVICE_PROVIDER', serviceName);
}
} else {
providerPromise = Promise.reject(
new YError('E_UNEXPECTED_STATE', serviceName, initializer),
);
}
if (initializerState.initializer[SPECIAL_PROPS.SINGLETON]) {
(
initializerState as SingletonInitializerStateDescriptor<any, any>
).singletonProviderLoadPromise =
providerPromise as unknown as Promise<void>;
} else {
(
initializerState as SiloedInitializerStateDescriptor<any, any>
).silosInstances[siloContext.index] = {
providerLoadPromise: providerPromise as unknown as Promise<void>,
};
}
const provider = await providerPromise;
if (
!provider ||
!(typeof provider === 'object') ||
!('service' in provider)
) {
debug('Provider has no `service` property:', serviceName);
throw new YError('E_BAD_SERVICE_PROVIDER', serviceName);
}
if (provider.fatalErrorPromise) {
debug('Registering service descriptor error promise:', serviceName);
siloContext.errorsPromises.push(provider.fatalErrorPromise);
}
if (initializerState.initializer[SPECIAL_PROPS.SINGLETON]) {
(
initializerState as SingletonInitializerStateDescriptor<any, any>
).singletonProvider = provider;
} else {
(
initializerState as SiloedInitializerStateDescriptor<any, any>
).silosInstances[siloContext.index] = { provider };
}
}
async _getAutoloader(
siloContext: SiloContext,
parentsNames: ServiceName[],
): Promise<
Autoloader<Initializer<unknown, Dependencies<unknown>>> | undefined
> {
// The auto loader must only have static dependencies
// and we have to do this check here to avoid inifinite loop
if (parentsNames.includes(AUTOLOAD)) {
debug(
`${parentsNames.join(
'->',
)}: Won't try to autoload autoloader dependencies...`,
);
return;
}
serviceDescriptor.preloaded = false;
const autoloaderState: SingletonInitializerStateDescriptor<any, any> =
this._initializersStates[AUTOLOAD];
return serviceDescriptor.promise;
if (!autoloaderState) {
return;
}
if (!('singletonProviderLoadPromise' in autoloaderState)) {
debug(`${parentsNames.join('->')}: Instanciating the autoloader...`);
// Trick to ensure the singletonProviderLoadPromise is set
let resolveAutoloder;
autoloaderState.singletonProviderLoadPromise = new Promise((_resolve) => {
resolveAutoloder = _resolve;
});
resolveAutoloder(
await this._loadProvider(siloContext, AUTOLOAD, parentsNames),
);
}
await autoloaderState.singletonProviderLoadPromise;
const autoloader = (await this._getServiceProvider(
siloContext,
AUTOLOAD,
)) as Provider<Autoloader<any>>;
debug(`${parentsNames.join('->')}: Loaded the autoloader...`);
if (!autoloader) {
throw new YError('E_UNEXPECTED_AUTOLOADER');
}
return autoloader.service;
}
/**
* Initialize a service descriptor
* @param {Object} siloContext
* Current execution silo context
* @param {String} serviceName
* Service name.
* @param {Object} options
* Options for service retrieval
* @param {Boolean} options.injectorContext
* Flag indicating the injection were initiated by the $injector
* @param {Boolean} options.autoloading
* Flag to indicating $autoload dependendencies on the fly loading.
* @return {Promise}
* Service dependencies hash promise.
*/
async _initializeServiceDescriptor(
siloContext: SiloContext<unknown>,
async _loadInitializer(
siloContext: SiloContext,
serviceName: ServiceName,
initializer: ProviderInitializer<Record<string, unknown>, unknown>,
{
autoloading,
injectorContext,
}: { autoloading: boolean; injectorContext: boolean },
): Promise<Provider<unknown>> {
let serviceDescriptor: Provider<unknown>;
parentsNames: ServiceName[],
): Promise<void> {
const initializerState = this._initializersStates[serviceName];
debug('Initializing a service descriptor:', serviceName);
debug(
`${[...parentsNames, serviceName].join('->')}: Loading an initializer...`,
);
try {
// A singleton service may use a reserved resource
// like a TCP socket. This is why we have to be aware
// of singleton services full shutdown before creating
// a new one
// At that point there should be an initialiser state
if (!initializerState) {
throw new YError('E_UNEXPECTED_INITIALIZER_STATE', serviceName);
}
await (this._singletonsServicesShutdownsPromises.get(serviceName) ||
Promise.resolve());
// Anyway delete any shutdown promise before instanciating
// a new service
this._singletonsServicesShutdownsPromises.delete(serviceName);
siloContext.servicesShutdownsPromises.delete(serviceName);
const servicesHash = await this._initializeDependencies(
siloContext,
serviceName,
initializer[SPECIAL_PROPS.INJECT],
{ injectorContext, autoloading },
// When no initializer try to autoload it
if (!('initializer' in initializerState)) {
debug(
`${[...parentsNames, serviceName].join(
'->',
)}: No registered initializer...`,
);
debug('Successfully gathered service dependencies:', serviceName);
if (initializerState.initializerLoadPromise) {
debug(
`${[...parentsNames, serviceName].join(
'->',
)}: Wait for pending initializer registration...`,
);
await initializerState.initializerLoadPromise;
} else {
debug(
`${[...parentsNames, serviceName].join(
'->',
)}: Try to autoload the initializer...`,
);
serviceDescriptor = await initializer(
initializer[SPECIAL_PROPS.INJECT].reduce(
(finalHash, dependencyDeclaration) => {
const { serviceName, mappedName } = parseDependencyDeclaration(
dependencyDeclaration,
);
initializerState.autoloaded = true;
finalHash[serviceName] = servicesHash[mappedName];
return finalHash;
// Trick to ensure the singletonProviderLoadPromise is set
let resolveInitializer, rejectInitializer;
initializerState.initializerLoadPromise = new Promise(
(_resolve, _reject) => {
resolveInitializer = _resolve;
rejectInitializer = _reject;
},
{},
),
);
);
if (!serviceDescriptor) {
debug('Provider did not return a descriptor:', serviceName);
return Promise.reject(new YError(E_BAD_SERVICE_PROVIDER, serviceName));
try {
const autoloader = await this._getAutoloader(siloContext, [
...parentsNames,
serviceName,
]);
if (!autoloader) {
debug(
`${parentsNames.join(
'->',
)}: No autoloader found, leaving initializer undefined...`,
);
initializerState.initializer = undefined;
resolveInitializer(undefined);
return;
}
const result = await autoloader(serviceName);
if (
typeof result !== 'object' ||
!('initializer' in result) ||
!('path' in result)
) {
throw new YError('E_BAD_AUTOLOADER_RESULT', serviceName, result);
}
const { initializer, path } = result;
debug(
`${[...parentsNames, serviceName].join(
'->',
)}: Loaded the initializer at path ${path}...`,
);
if (initializer[SPECIAL_PROPS.NAME] !== serviceName) {
throw new YError(
'E_AUTOLOADED_INITIALIZER_MISMATCH',
serviceName,
initializer[SPECIAL_PROPS.NAME],
);
}
initializerState.dependents.push({
silo: siloContext.index,
name: AUTOLOAD,
optional: false,
});
initializerState.initializer = initializer;
this._buildInitializerState(initializerState, initializer);
resolveInitializer(initializer);
return;
} catch (err) {
if ((err as YError).code === 'E_AULOADER_DEPENDS_ON_AUTOLOAD') {
initializerState.initializer = undefined;
rejectInitializer(err);
await initializerState.initializerLoadPromise;
return;
}
if (!['E_UNMATCHED_DEPENDENCY'].includes((err as YError).code)) {
initializerState.initializer = undefined;
rejectInitializer(
YError.wrap(
err as Error,
'E_BAD_AUTOLOADED_INITIALIZER',
serviceName,
),
);
await initializerState.initializerLoadPromise;
return;
}
debug(
`${[...parentsNames, serviceName].join(
'->',
)}: Could not autoload the initializer...`,
err,
);
initializerState.initializer = undefined;
resolveInitializer(undefined);
await initializerState.initializerLoadPromise;
}
return;
}
debug('Successfully initialized a service descriptor:', serviceName);
if (serviceDescriptor.fatalErrorPromise) {
debug('Registering service descriptor error promise:', serviceName);
siloContext.errorsPromises.push(serviceDescriptor.fatalErrorPromise);
}
siloContext.servicesDescriptors.set(
serviceName,
Promise.resolve(serviceDescriptor),
);
} catch (err) {
debug(
'Error initializing a service descriptor:',
serviceName,
(err as Error).stack || 'no_stack_trace',
);
if (E_UNMATCHED_DEPENDENCY === (err as YError).code) {
throw YError.wrap(
err as Error,
E_UNMATCHED_DEPENDENCY,
...[serviceName].concat((err as YError).params),
} else {
if (initializerState.initializer) {
debug(
`${[...parentsNames, serviceName].join('->')}: Initializer ready...`,
);
if (initializer[SPECIAL_PROPS.TYPE] === 'constant') {
const provider = initializerState.initializer[SPECIAL_PROPS.VALUE];
(
initializerState as SingletonInitializerStateDescriptor<any, any>
).singletonProvider = provider;
(
initializerState as SingletonInitializerStateDescriptor<any, any>
).singletonProviderLoadPromise = Promise.resolve(provider);
}
if (initializerState.initializer[SPECIAL_PROPS.SINGLETON]) {
const singletonInitializerState =
initializerState as SingletonInitializerStateDescriptor<any, any>;
if (!('singletonProviderLoadPromise' in singletonInitializerState)) {
singletonInitializerState.singletonProviderLoadPromise =
this._loadProvider(siloContext, serviceName, parentsNames);
}
await singletonInitializerState.singletonProviderLoadPromise;
} else {
const siloedInitializerState =
initializerState as SiloedInitializerStateDescriptor<any, any>;
if (!siloedInitializerState.silosInstances[siloContext.index]) {
siloedInitializerState.silosInstances[siloContext.index] = {
providerLoadPromise: this._loadProvider(
siloContext,
serviceName,
parentsNames,
),
};
}
await siloedInitializerState.silosInstances[siloContext.index]
.providerLoadPromise;
}
} else {
debug(
`${[...parentsNames, serviceName].join(
'->',
)}: Could not find the initializer...`,
);
initializerState.initializer = undefined;
(
initializerState as SingletonInitializerStateDescriptor<any, any>
).singletonProvider = NO_PROVIDER;
}
throw err;
}
return serviceDescriptor;
}
async _resolveDependencies(
siloContext: SiloContext,
loadingServices: ServiceName[],
parentsNames: ServiceName[],
): Promise<void> {
debug(
`Initiating a dependencies load round for silo "${siloContext.index}"'.`,
);
if (this._options.sequential) {
for (const loadingService of loadingServices) {
await this._loadInitializer(siloContext, loadingService, parentsNames);
}
} else {
await Promise.all(
loadingServices.map((loadingService) =>
this._loadInitializer(siloContext, loadingService, parentsNames),
),
);
}
}
/**
* Initialize a service dependencies
* @param {Object} siloContext
* Current execution silo siloContext
* @param {String} serviceName
* Service name.
* @param {String} servicesDeclarations
* Dependencies declarations.
* @param {Object} options
* Options for service retrieval
* @param {Boolean} options.injectorContext
* Flag indicating the injection were initiated by the $injector
* @param {Boolean} options.autoloading
* Flag to indicating $autoload dependendencies on the fly loading.
* Destroy the Knifecycle instance
* @return {Promise}
* Service dependencies hash promise.
* Full destruction promise
* @example
*
* import Knifecycle, { constant } from 'knifecycle'
*
* const $ = new Knifecycle();
*
* $.register(constant('ENV', process.env));
* $.run(['ENV'])
* .then(({ ENV }) => {
* // Here goes your code
*
* // Finally destroy the instance
* $.destroy()
* })
*/
async _initializeDependencies(
siloContext: SiloContext<unknown>,
serviceName: ServiceName,
servicesDeclarations: DependencyDeclaration[],
{
injectorContext = false,
autoloading = false,
}: { autoloading: boolean; injectorContext: boolean },
): Promise<Dependencies> {
debug('Initializing dependencies:', serviceName, servicesDeclarations);
const servicesDescriptors: (Provider<unknown> | undefined)[] =
await Promise.all(
servicesDeclarations.map(async (serviceDeclaration) => {
const { mappedName, optional } =
parseDependencyDeclaration(serviceDeclaration);
async destroy(): Promise<void> {
this._shutdownPromise =
this._shutdownPromise ||
Promise.all(
Object.keys(this._silosContexts).map(async (siloIndex) => {
const siloContext = this._silosContexts[siloIndex];
const $dispose = (
(await this._getServiceProvider(
siloContext,
DISPOSE,
)) as Provider<Disposer>
)?.service;
try {
const serviceDescriptor = await this._getServiceDescriptor(
siloContext,
mappedName,
{
injectorContext,
autoloading,
},
);
return serviceDescriptor;
} catch (err) {
if (
optional &&
[
'E_UNMATCHED_DEPENDENCY',
E_AUTOLOADER_DYNAMIC_DEPENDENCY,
].includes((err as YError).code)
) {
debug(
'Optional dependency not found:',
serviceDeclaration,
(err as Error).stack || 'no_stack_trace',
);
return;
}
throw err;
}
return $dispose();
}),
);
debug(
'Initialized dependencies descriptors:',
serviceName,
servicesDeclarations,
servicesDescriptors,
);
siloContext.servicesSequence.push(
servicesDeclarations
.filter((_, index) => servicesDescriptors[index])
.map(_pickMappedNameFromDeclaration),
);
).then(() => undefined);
const services = await Promise.all(
servicesDescriptors.map(async (serviceDescriptor) => {
if (!serviceDescriptor) {
return undefined;
}
return serviceDescriptor.service;
}),
);
debug('Shutting down Knifecycle instance.');
return services.reduce<Record<string, unknown>>((hash, service, index) => {
const mappedName = _pickMappedNameFromDeclaration(
servicesDeclarations[index],
);
hash[mappedName] = service;
return hash;
}, {});
return this._shutdownPromise;
}

@@ -1213,10 +1481,2 @@ }

function _pickMappedNameFromDeclaration(
dependencyDeclaration: DependencyDeclaration,
): ServiceName {
const { mappedName } = parseDependencyDeclaration(dependencyDeclaration);
return mappedName;
}
function _applyShapes(shapes, serviceName) {

@@ -1276,3 +1536,3 @@ return shapes.reduce((shapedService, shape) => {

if (!classes[style.className]) {
throw new YError(E_BAD_CLASS, style.className, serviceName);
throw new YError('E_BAD_CLASS', style.className, serviceName);
}

@@ -1286,3 +1546,3 @@ classesApplications[serviceName] = style.className;

if (!classes[style.className]) {
throw new YError(E_BAD_CLASS, style.className, dependedServiceName);
throw new YError('E_BAD_CLASS', style.className, dependedServiceName);
}

@@ -1294,34 +1554,1 @@ classesApplications[dependedServiceName] = style.className;

}
function serviceAdapter<
S,
T extends ServiceInitializer<Record<string, unknown>, S>,
>(
serviceName: ServiceName,
initializer: T,
dependenciesHash: T extends ServiceInitializer<infer D, unknown> ? D : never,
): Promise<Provider<S>> {
const servicePromise = initializer(dependenciesHash);
if (!servicePromise || !servicePromise.then) {
throw new YError(E_BAD_SERVICE_PROMISE, serviceName);
}
return servicePromise.then((_service_) => ({
service: _service_,
}));
}
function _buildFinalHash(
servicesHash: { [name: string]: unknown },
dependenciesDeclarations: DependencyDeclaration[],
): { [name: string]: unknown } {
return dependenciesDeclarations.reduce((finalHash, dependencyDeclaration) => {
const { serviceName, mappedName } = parseDependencyDeclaration(
dependencyDeclaration,
);
finalHash[serviceName] = servicesHash[mappedName];
return finalHash;
}, {});
}

@@ -0,1 +1,3 @@

/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */
import { describe, test } from '@jest/globals';

@@ -2,0 +4,0 @@ import assert from 'assert';

@@ -0,1 +1,2 @@

/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint @typescript-eslint/ban-types:0 */

@@ -2,0 +3,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

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc