A library for managing asynchronous disposal of objects with dependencies, with built-in support for InversifyJS.
- Asynchronous disposal of objects in dependency order.
- Circular dependency detection (circular dependency is strongly discouraged).
- Use existing interfaces
Disposable
andAsyncDisposable
. - Built-in support of extracting dependencies from the
@inject
annotation of InversifyJS. - Also works without InversifyJS.
In many cases, we need to dispose of objects in a specific order, especially when dealing with asynchronous operations. For example, say UserService
depends on DatabaseService
, UserService
is only usable when DatabaseService
is ready. Therefore, UserService
should be disposed before DatabaseService
. Otherwise, the state "UserService
is still marked as available, but DatabaseService
is destroyed" is possible, allowing other part of the program to use UserService
when its dependency DatabaseService
is no longer available.
Manually managing disposal order can be error-prone and difficult to maintain. While "disposing objects in the reverse order of their creation" sounds like exactly the job of dependency injection library, none of them in JS world provides this feature -- at least not the ones I know of. In the Java empire, this is part of the dependency injection features of Spring Framework.
Related discussions:
Firstly follow the GitHub Packages Registry documentation to set up authentication to the GitHub Package Registry.
Then, add the following line to your .npmrc
file under your project root:
@unlib-js:registry=https://npm.pkg.github.com
pnpm add @unlib-js/depi
yarn add @unlib-js/depi
npm install @unlib-js/depi
import { setTimeout } from 'node:timers/promises'
import { destroy } from '@unlib-js/depi'
import DependsOn from '@unlib-js/depi/decorators/DependsOn'
import Dependency from '@unlib-js/depi/decorators/Dependency'
class RemoteConfigService implements AsyncDisposable {
public async [Symbol.asyncDispose]() {
console.log('Destroyed DatabaseService')
}
}
const remoteConfigService = new RemoteConfigService()
class DatabaseService implements AsyncDisposable {
@Dependency()
private readonly remoteConfigService = remoteConfigService
public async [Symbol.asyncDispose]() {
console.log('Destroyed DatabaseService')
}
}
const databaseService = new DatabaseService()
@DependsOn(['databaseService'])
class UserService implements AsyncDisposable {
private readonly databaseService = databaseService
public async [Symbol.asyncDispose]() {
await setTimeout(1000) // Simulate some async work
console.log('Destroyed UserService')
}
}
const userService = new UserService()
// Now `userService` depends on its property `databaseService`
await destroy({
instances: [databaseSerivce, userService],
onCircularDependencyDetected(stack, graph) {
console.warn('Circular dependency detected:', stack)
}
})
// Output:
// Destroyed UserService
// Destroyed DatabaseService
// Destroyed RemoteConfigService
import { setTimeout } from 'node:timers/promises'
import { Container, injectable, inject } from 'inversify'
import { destroy } from '@unlib-js/depi'
import DependsOn from '@unlib-js/depi/decorators/DependsOn'
import getDeps from '@unlib-js/depi/helpers/inversify/getDeps'
@injectable()
class RemoteConfigService implements AsyncDisposable {
public async [Symbol.asyncDispose]() {
console.log('Destroyed RemoteConfigService')
}
}
@DependsOn(getDeps(DatabaseService))
@injectable()
class DatabaseService implements AsyncDisposable {
public constructor(
@inject(RemoteConfigService)
private readonly remoteConfigService: RemoteConfigService,
) {}
public async [Symbol.asyncDispose]() {
await setTimeout(500) // Simulate some async work
console.log('Destroyed DatabaseService')
}
}
@DependsOn(getDeps(UserService))
@injectable()
class UserService {
@inject(RemoteConfigService)
private readonly remoteConfigService!: RemoteConfigService
public constructor(
@inject(DatabaseService)
databaseService: DatabaseService,
) {
// ...
}
public async [Symbol.asyncDispose]() {
await setTimeout(1000) // Simulate some async work
console.log('Destroyed UserService')
}
}
const container = new Container()
container.bind(RemoteConfigService).toSelf().inSingletonScope()
container.bind(DatabaseService).toSelf().inSingletonScope()
container.bind(UserService).toSelf().inSingletonScope()
const userService = await container.getAsync(UserService)
// ...
// During application shutdown:
await destroy({
instances: [
container.get(RemoteConfigService),
container.get(DatabaseService),
container.get(UserService),
],
onCircularDependencyDetected(stack, graph) {
console.warn('Circular dependency detected:', stack)
}
})
// Output:
// Destroyed UserService
// Destroyed DatabaseService
// Destroyed RemoteConfigService
pnpm example basic
pnpm example inversify
For more examples, please refer to the tests, e.g., this test.
pnpm i && pnpm build
pnpm typedoc
pnpm test
See here.
- Build a reversed dependency graph from the provided instances.
- Traverse the graph in leaf-first order. For each node, enqueue the following async disposal task:
- Wait for all dependants of the node, i.e., child nodes, to complete their disposal.
- Dispose the node itself.
- Wait for all queued tasks to complete.
Circular dependencies are generally discouraged. However, if there is a loop, the library invokes the onCircularDependencyDetected
callback with the stack of detected circular dependency (a loop path), and ignores the edge that caused the loop as if it were not there.
TODO