@shahen.poghosyan/awilix
Version:
Extremely powerful dependency injection container.
363 lines • 13.3 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const util = require("util");
const list_modules_1 = require("./list-modules");
const load_modules_1 = require("./load-modules");
const resolvers_1 = require("./resolvers");
const utils_1 = require("./utils");
const injection_mode_1 = require("./injection-mode");
const lifetime_1 = require("./lifetime");
const errors_1 = require("./errors");
/**
* Family tree symbol.
* @type {Symbol}
*/
const FAMILY_TREE = Symbol('familyTree');
/**
* Roll Up Registrations symbol.
* @type {Symbol}
*/
const ROLL_UP_REGISTRATIONS = Symbol('rollUpRegistrations');
/**
* Creates an Awilix container instance.
*
* @param {Function} options.require
* The require function to use. Defaults to require.
*
* @param {string} options.injectionMode
* The mode used by the container to resolve dependencies. Defaults to 'Proxy'.
*
* @return {object}
* The container.
*/
function createContainer(options, parentContainer) {
options = Object.assign({ injectionMode: injection_mode_1.InjectionMode.PROXY }, options);
// The resolution stack is used to keep track
// of what modules are being resolved, so when
// an error occurs, we have something to present
// to the poor developer who fucked up.
let resolutionStack = [];
// For performance reasons, we store
// the rolled-up registrations when starting a resolve.
let computedRegistrations = null;
// Internal registration store for this container.
const registrations = {};
/**
* The `Proxy` that is passed to functions so they can resolve their dependencies without
* knowing where they come from. I call it the "cradle" because
* it is where registered things come to life at resolution-time.
*/
const cradle = new Proxy({
[util.inspect.custom]: inspectCradle
}, {
/**
* The `get` handler is invoked whenever a get-call for `container.cradle.*` is made.
*
* @param {object} target
* The proxy target. Irrelevant.
*
* @param {string} name
* The property name.
*
* @return {*}
* Whatever the resolve call returns.
*/
get: (target, name) => resolve(name),
/**
* Setting things on the cradle throws an error.
*
* @param {object} target
* @param {string} name
*/
set: (_target, name, value) => {
throw new Error(`Attempted setting property "${name}" on container cradle - this is not allowed.`);
},
/**
* Used for `Object.keys`.
*/
ownKeys() {
return Array.from(cradle);
},
/**
* Used for `Object.keys`.
*/
getOwnPropertyDescriptor(target, key) {
const regs = rollUpRegistrations();
if (Object.getOwnPropertyDescriptor(regs, key)) {
return {
enumerable: true,
configurable: true
};
}
return undefined;
}
});
// The container being exposed.
const container = {
options,
cradle: cradle,
inspect,
cache: new Map(),
loadModules,
createScope,
register: register,
build,
resolve,
has,
dispose,
[util.inspect.custom]: inspect,
// tslint:disable-next-line
[ROLL_UP_REGISTRATIONS]: rollUpRegistrations,
get registrations() {
return rollUpRegistrations();
}
};
// Track the family tree.
const familyTree = parentContainer
? [container].concat(parentContainer[FAMILY_TREE])
: [container];
container[FAMILY_TREE] = familyTree;
// We need a reference to the root container,
// so we can retrieve and store singletons.
const rootContainer = utils_1.last(familyTree);
return container;
/**
* Used by util.inspect (which is used by console.log).
*/
function inspect(depth, opts) {
return `[AwilixContainer (${parentContainer ? 'scoped, ' : ''}registrations: ${Object.keys(container.registrations).length})]`;
}
/**
* Rolls up registrations from the family tree.
* This is cached until `bustCache` clears it.
*
* @param {boolean} bustCache
* Forces a recomputation.
*
* @return {object}
* The merged registrations object.
*/
function rollUpRegistrations(bustCache = false) {
if (computedRegistrations && !bustCache) {
return computedRegistrations;
}
computedRegistrations = Object.assign({}, (parentContainer &&
parentContainer[ROLL_UP_REGISTRATIONS](bustCache)), registrations);
return computedRegistrations;
}
/**
* Used for providing an iterator to the cradle.
*/
function* registrationNamesIterator() {
const registrations = rollUpRegistrations();
for (const registrationName in registrations) {
yield registrationName;
}
}
/**
* Creates a scoped container.
*
* @return {object}
* The scoped container.
*/
function createScope() {
return createContainer(options, container);
}
/**
* Adds a registration for a resolver.
*/
function register(arg1, arg2) {
const obj = utils_1.nameValueToObject(arg1, arg2);
const keys = [...Object.keys(obj), ...Object.getOwnPropertySymbols(obj)];
for (const key of keys) {
const value = obj[key];
registrations[key] = value;
}
// Invalidates the computed registrations.
computedRegistrations = null;
return container;
}
/**
* Returned to `util.inspect` when attempting to resolve
* a custom inspector function on the cradle.
*/
function inspectCradle() {
return '[AwilixContainer.cradle]';
}
/**
* Resolves the registration with the given name.
*
* @param {string | symbol} name
* The name of the registration to resolve.
*
* @param {ResolveOptions} resolveOpts
* The resolve options.
*
* @return {any}
* Whatever was resolved.
*/
function resolve(name, resolveOpts) {
resolveOpts = resolveOpts || {};
if (!resolutionStack.length) {
// Root resolve busts the registration cache.
rollUpRegistrations(true);
}
try {
// Grab the registration by name.
const resolver = computedRegistrations[name];
if (resolutionStack.indexOf(name) > -1) {
throw new errors_1.AwilixResolutionError(name, resolutionStack, 'Cyclic dependencies detected.');
}
// Used in console.log.
if (name === 'constructor') {
return createContainer;
}
if (!resolver) {
// The following checks ensure that console.log on the cradle does not
// throw an error (issue #7).
if (name === util.inspect.custom || name === 'inspect') {
return inspectCradle;
}
// Edge case: Promise unwrapping will look for a "then" property and attempt to call it.
// Return undefined so that we won't cause a resolution error. (issue #109)
if (name === 'then') {
return undefined;
}
// When using `Array.from` or spreading the cradle, this will
// return the registration names.
if (name === Symbol.iterator) {
return registrationNamesIterator;
}
if (resolveOpts.allowUnregistered) {
return undefined;
}
throw new errors_1.AwilixResolutionError(name, resolutionStack);
}
// Pushes the currently-resolving module name onto the stack
resolutionStack.push(name);
// Do the thing
let cached;
let resolved;
switch (resolver.lifetime || lifetime_1.Lifetime.TRANSIENT) {
case lifetime_1.Lifetime.TRANSIENT:
// Transient lifetime means resolve every time.
resolved = resolver.resolve(container);
break;
case lifetime_1.Lifetime.SINGLETON:
// Singleton lifetime means cache at all times, regardless of scope.
cached = rootContainer.cache.get(name);
if (!cached) {
resolved = resolver.resolve(container);
rootContainer.cache.set(name, { resolver, value: resolved });
}
else {
resolved = cached.value;
}
break;
case lifetime_1.Lifetime.SCOPED:
// Scoped lifetime means that the container
// that resolves the registration also caches it.
// When a registration is not found, we travel up
// the family tree until we find one that is cached.
cached = container.cache.get(name);
if (cached !== undefined) {
// We found one!
resolved = cached.value;
break;
}
// If we still have not found one, we need to resolve and cache it.
resolved = resolver.resolve(container);
container.cache.set(name, { resolver, value: resolved });
break;
default:
throw new errors_1.AwilixResolutionError(name, resolutionStack, `Unknown lifetime "${resolver.lifetime}"`);
}
// Pop it from the stack again, ready for the next resolution
resolutionStack.pop();
return resolved;
}
catch (err) {
// When we get an error we need to reset the stack.
resolutionStack = [];
throw err;
}
}
/**
* Checks if the registration with the given name exists.
*
* @param {string | symbol} name
* The name of the registration to resolve.
*
* @return {boolean}
* Whether or not the registration exists.
*/
function has(name) {
return name in rollUpRegistrations();
}
/**
* Given a registration, class or function, builds it up and returns it.
* Does not cache it, this means that any lifetime configured in case of passing
* a registration will not be used.
*
* @param {Resolver|Class|Function} targetOrResolver
* @param {ResolverOptions} opts
*/
function build(targetOrResolver, opts) {
if (targetOrResolver && targetOrResolver.resolve) {
return targetOrResolver.resolve(container);
}
const funcName = 'build';
const paramName = 'targetOrResolver';
errors_1.AwilixTypeError.assert(targetOrResolver, funcName, paramName, 'a registration, function or class', targetOrResolver);
errors_1.AwilixTypeError.assert(typeof targetOrResolver === 'function', funcName, paramName, 'a function or class', targetOrResolver);
const resolver = utils_1.isClass(targetOrResolver)
? resolvers_1.asClass(targetOrResolver, opts)
: resolvers_1.asFunction(targetOrResolver, opts);
return resolver.resolve(container);
}
/**
* Binds `lib/loadModules` to this container, and provides
* real implementations of it's dependencies.
*
* Additionally, any modules using the `dependsOn` API
* will be resolved.
*
* @see lib/loadModules.js documentation.
*/
function loadModules(globPatterns, opts) {
const _loadModulesDeps = {
require: options.require ||
function (uri) {
try {
return require(uri);
}
catch (e) {
console.log('failed to require file ', uri, e);
throw e;
}
},
listModules: list_modules_1.listModules,
container
};
load_modules_1.loadModules(_loadModulesDeps, globPatterns, opts);
return container;
}
/**
* Disposes this container and it's children, calling the disposer
* on all disposable registrations and clearing the cache.
*/
function dispose() {
const entries = Array.from(container.cache.entries());
container.cache.clear();
return Promise.all(entries.map(([name, entry]) => {
const { resolver, value } = entry;
const disposable = resolver;
if (disposable.dispose) {
return Promise.resolve().then(() => disposable.dispose(value));
}
return Promise.resolve();
})).then(() => undefined);
}
}
exports.createContainer = createContainer;
//# sourceMappingURL=container.js.map