holoscope v0.9.0
Dependency injection tool for Typescript projects.
Inspired by Awilix and AWS CDK Constructs.
Installation
With npm:
npm install holoscopeGetting Started
If you're familiar with Awilix or similar IoC libraries, you should check out this section
Holoscope has two key concepts: Scopes and Resolvers. You wrap your dependencies (service classes, configuration objects, etc) in an appropriate resolver and pass them to a Scope, which manages passing peer dependencies to each resolver. Take this simple example:
import { asClass, Scope } from 'holoscope'
class PostService {
constructor(private container: { database: Database }) {}
async getPosts() {
return this.container.database.posts.getAll()
}
}
const scope = new Scope({
database: asClass(Database, { cached: true }),
postService: asClass(PostService, { cached: true }),
})
const posts = await scope.container.postService.getPosts()Scope.container is a proxy that handles dependency resolution at access-time. It is passed to each resolver's resolve method. asFunction and asClass resolvers pass the container to the provided factory function or class respectively.
In the example above, the database is instantiated inside the getPosts method of PostService. After that, it gets cached, and reused in any subsequent access through the container.
Note: values that are not a
Resolverare wrapped inasValue()at registration. This allows to register plain values without wrapping them.If you want to create your own resolver with custom logic, look here.
Building your scopes
The Scope class should be extended to build your scopes:
type GeneralContainer = {
database: Database
postService: PostService
}
class GeneralScope extends Scope<GeneralContainer> {
constructor() {
super({
database: asClass(Database, { cached: true }),
postService: asClass(PostService, { cached: true }),
})
}
}
const scope = new GeneralScope()Extending scopes
Notice that the values in the container type provided to scope's generic parameter (GeneralContainer above) don't have to be specific implementations, but interfaces instead.
This allows to extend and swap out the dependencies:
class DevelopmentScope extends GeneralScope {
constructor() {
super()
this.register({
// DevelopmentDatabase satisfies Database
database: asClass(DevelopmentDatabase, { cached: true }),
})
}
}Expanding scopes
To expand a scope (extend and add dependencies, not just swap them out), consider using ExtendedInjection generic type as a constructor input in the following pattern:
interface BaseContainer {
name: string
}
class BaseScope<TExtended extends ExampleContainer> extends Scope<TExtended> {
constructor(extended: ExtendedInjection<ExampleContainer, TExtended>) {
const registrations: Injection<ExampleContainer> = {
}
super({
name: 'example',
...extended,
} as Injection<TExtended>)
}
}
class ExtendedScope extends BaseScope<ExtendedContainer> {
constructor() {
super({
greeting: asFunction(({ name }) => `Hello, ${name}!`),
})
}
}
new ExtendedScope().container.greeting // 'Hello, World!'Disposing scopes and their resolvers
For asFunction and asClass resolvers (as well as possibly for custom resolvers), there is an option to provide a disposer function.
Scope.dispose() is an async method, which internally awaits every resolver's disposer.
This is useful to close database connections, write logs, etc. at the end of a process.
For
asFunctionandasClassresolvers withcached: true, disposers are only called if the dependency was resolved before.
Comparison with Awilix
- Different terminology for similar concepts/types:
awilix.Container -> holoscope.Scope;awilix.Container.cradle -> holoscope.Scope.container. - All registration must be provided at
Scopeinit, ensuring type-safety. - Factory resolvers (
asFunction,asClass) are not bound to the scope. Cache is handled inside the resolvers themselves. - No auto-loading modules.
- Instead of "child" containers, scopes can be extended, overwriting and adding new dependencies in a flat internal structure.
Built-in resolvers and helper functions
Holoscope includes the following general-purpose resolvers:
asValue- used internally to wrap non-resolver values. Can be used explicitly.asFunction,asClass- factory resolvers. Optionally handle cache and disposing. Allow injecting per-dependency peers withinjectoption.aliasTo- returns a dependency of a passed name from the container. Beware recursive loops - do not pass the name this resolver is registered itself.asResolvers- pass an object of resolvers. Used to nest dependencies.
There are also helper functions asCachedFunction and asCachedClass, that are simple shorthands for as___(factory, { cached: true })
Custom resolvers
You can create your own resolvers with custom logic, e.g.:
import { Container, IS_RESOLVER, Resolver } from 'holoscope'
class CustomResolver<T = unknown> implements Resolver<T> {
readonly [IS_RESOLVER] = true
resolve(container: Container): T {
// custom logic, that returns T
}
}Resolvers have to:
- have a
resolvemethod that accepts a container and returns the value - have
IS_RESOLVERproperty set totrue. Resolver doesn't necessarily have to be a class instance.NOTE:
IS_RESOLVERis a Symbol imported from the library — this prevents prop name conflicts. Make sure you are setting[IS_RESOLVER] = trueand notIS_RESOLVER = true.
A resolver can have an optional dispose method that accepts the entire container. However, it is recommended to only interact with the dependency the resolver represents in custom disposers, to avoid accessing a peer dependency after it already was disposed.
API Reference
TODO
Contributing
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
9 months ago
10 months ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
4 years ago
4 years ago
4 years ago