@supercharge/container
Version:
The Supercharge container package
219 lines (218 loc) • 7.16 kB
JavaScript
import Map from '@supercharge/map';
import { Str } from '@supercharge/strings';
import { className, isClass } from '@supercharge/classes';
import { tap, upon, isFunction } from '@supercharge/goodies';
export class Container {
/**
* Stores the container bindings.
*/
bindings;
/**
* Stores the registered aliases.
*/
aliases;
/**
* Stores the singleton instances.
*/
singletons;
/**
* Create a new container instance.
*/
constructor() {
this.aliases = new Map();
this.bindings = new Map();
this.singletons = new Map();
}
/**
* Register a binding in the container.
*/
bind(namespace, factory, options) {
this.ensureNamespace(namespace);
const { singleton = false } = options ?? {};
if (!isFunction(factory)) {
throw new Error(`container.bind(namespace, factory) expects the second argument to be a function. Received ${typeof factory}`);
}
return tap(this, () => {
this.bindings.set(this.resolveNamespace(namespace), { factory, isSingleton: singleton });
});
}
/**
* Ensure the given `namespace` is a string or a class constructor.
*/
ensureNamespace(namespace) {
if (isClass(namespace)) {
return;
}
if (Str(namespace).isNotEmpty()) {
return;
}
throw new Error('Cannot bind empty namespace to the container');
}
/**
* Register a shared binding (singleton) in the container.
*/
singleton(namespace, factory) {
return this.bind(namespace, factory, { singleton: true });
}
/**
* Determine whether the given `namespace` is bound in the container.
*/
hasBinding(namespace) {
return this.isAlias(namespace) || this.isSingleton(namespace) || this.bindings.has(this.resolveNamespace(namespace));
}
/**
* Determine whether the given `namespace` is bound as a singleton in the container.
*/
hasSingletonBinding(namespace) {
return this.singletons.has(this.resolveNamespace(namespace));
}
/**
* Returns the resolved namespace identifier as a string.
*/
resolveNamespace(namespace) {
return isClass(namespace)
? className(namespace)
: String(namespace);
}
/**
* Determine whether the given `namespace` is a singleton.
*/
isSingleton(namespace) {
return this.hasSingletonBinding(namespace) || this.bindings.contains((key, binding) => {
return key === this.resolveNamespace(namespace) && binding.isSingleton;
});
}
make(namespace) {
if (this.isAlias(namespace)) {
namespace = this.getAlias(namespace);
}
/**
* If the namespace exists as a singleton, we’ll return the instance
* without instantiating a new one. This way, the same instance
* is reused when requesting it from the container.
*/
if (this.hasSingletonBinding(namespace)) {
return this.singletons.get(this.resolveNamespace(namespace));
}
const instance = this.build(namespace);
/**
* If the namespace is expected to be a singleton, we’ll cache the instance
* in memory for future calls. Then, the cached instance will be returned.
*/
if (this.isSingleton(namespace)) {
this.singletons.set(this.resolveNamespace(namespace), instance);
}
return instance;
}
/**
* Run the factory function for the given binding that resolves the related instance.
*/
build(namespace) {
return upon(this.getFactoryFor(namespace), factory => {
return factory(this);
});
}
/**
* Returns the factory callback for the given namespace.
*/
getFactoryFor(namespace) {
if (this.hasBinding(namespace)) {
return this.resolveFactoryFor(namespace);
}
if (isClass(namespace)) {
return this.createFactoryFor(namespace);
}
throw new Error(`Missing container binding for the given namespace "${this.resolveNamespace(namespace)}"`);
}
/**
* Returns a factory function for the given namespace.
*/
resolveFactoryFor(namespace) {
const name = this.resolveNamespace(namespace);
return upon(this.bindings.get(name), binding => {
return binding?.factory;
});
}
/**
* Returns a factory function for the given class constructor.
*/
createFactoryFor(Constructor) {
return (container) => {
return new Constructor(container);
};
}
/**
* Determine whether the given `namespace` is an alias.
*/
getAlias(namespace) {
const abstract = this.resolveNamespace(namespace);
for (const [alias, aliases] of this.aliases.entries()) {
if (aliases.includes(abstract)) {
return alias;
}
}
throw new Error(`No alias registered for the given "${abstract}"`);
}
/**
* Determine whether the given `namespace` is an alias.
*/
isAlias(namespace) {
try {
this.getAlias(namespace);
return true;
}
catch (error) {
return false;
}
}
/**
* Alias a binding to a different name.
*/
alias(namespace, alias) {
if (!namespace) {
throw new Error('You must provide a source namespace as the first argument when creating a container alias.');
}
if (!alias) {
throw new Error('You must provide an alias name as the second argument when creating a container alias.');
}
const resolvedAlias = this.resolveNamespace(alias);
const resolvedNamespace = this.resolveNamespace(namespace);
if (resolvedAlias === resolvedNamespace) {
throw new Error(`"${resolvedNamespace}" is an alias for itself`);
}
return tap(this, () => {
this.addAlias(resolvedNamespace, resolvedAlias);
});
}
/**
* Assign the given `alias` to the concrete `namespace`.
*/
addAlias(namespace, alias) {
const aliases = this.aliases.getOrDefault(namespace, []);
aliases.push(alias);
this.aliases.set(namespace, aliases);
return this;
}
/**
* Remove a resolved instance from the (singleton) cache.
*/
forgetInstance(namespace) {
if (!namespace) {
throw new Error('You must provide a "namespace" as the first argument when calling container.forgetInstance(namespace).');
}
if (this.isAlias(namespace)) {
namespace = this.getAlias(namespace);
}
this.singletons.delete(this.resolveNamespace(namespace));
return this;
}
/**
* Flush all bindings and resolved instances from the containter.
*/
flush() {
return tap(this, () => {
this.bindings = new Map();
this.singletons = new Map();
});
}
}