jasc
Version:
Jasc Another Service Container
98 lines (85 loc) • 4.25 kB
text/typescript
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
}
}