UNPKG

jasc

Version:

Jasc Another Service Container

98 lines (85 loc) 4.25 kB
import { Tree, Node, createTree } from './tree' /** * A provider function is a function that takes a container where your dependencies are defined (at some point), and must return the services that you provide. * @template Services The services the provider should provide * @template Dependencies The dependencies the provider needs to define the `Services` */ export type ContainerProvider<Services, Dependencies = {}> = { (container: Container<Services, Dependencies>): Readonly<Services> } /** * * @template P The services the container should hold * @template Dependencies **DO NOT SET!* This template is only used internally for `ContainerProvider<Services, Dependencies>`'s */ export default class Container<P = {[name: string]: unknown}, Dependencies = P>{ private _tree: Tree = createTree() private _current: Node | null = null /** * Use a provider function that provides some of services defined in the template `<P>` * @param provider see `ContainerProvider` */ use<S, C extends this>(provider: ContainerProvider<S, Dependencies>): Readonly<C & S>{ return provider(this as any) as any } /** * Define a service for the container to serve. * The service will be defined as a property on the service, and will be lazily constructed. * The construction of the service takes place the first time it is resolved (read). * If a circular dependency is detected, an Error is thrown. * @param name Name of the service * @param factory The service factory * @returns The container * @throws {TypeError} if name is null, undefined or not a string, or if factory is null, undefined or not a function */ serve<T extends P[K], K extends keyof Omit<P, keyof this>, C extends this>(name: K, factory: (container: Readonly<Omit<P & Dependencies, K>>) => T): Readonly<C & Pick<P, K>> { if (!name || typeof name !== 'string') throw new TypeError(`'name' must be defined`) if (!factory) throw new TypeError(`'factory' must be defined`) Object.defineProperty(this, name, { get: (): T => { const tree = this._tree if (tree.has(name)) { const node = tree.get<T>(name) // the _current property is the parent to the service currently being resolved // if _current is not null then we're loading a dependency that has not yet been loaded if (this._current) { node.parents.add(this._current) this._current.children.add(node) } // factory has not yet returned, so we're in a resolve stack where we have looped back on ourselves (since tree.has(name) is true) if (node.value === undefined) { const parents = [name as string] node.traverseParents(p => { if(p.key === node.key) return true parents.push(p.key) }) console.warn(`Detected circular dependency: ${name} -> ${parents.reverse().join(' -> ')}`) throw new Error(`Circular dependency detected while resolving ${name}`) } // the service we're resolving has been initialized previously. return node.value } const parent = this._current const node = this._current = tree.add<T>(name, parent) const instance = factory(this as any) if(instance === undefined) throw new Error('factory returned undefined') this._current = parent return node.value = instance }, configurable: false, enumerable: true }) return this as any } /** * Dumps the loaded services to the console. */ dump(): this { this._tree.dump() return this } }