UNPKG

@supercharge/container

Version:

The Supercharge container package

219 lines (218 loc) 7.16 kB
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(); }); } }