Dependency Injection

Vinyl uses the most common form of dependency injection; requesting dependencies through constructors.

Class constructors have a deps parameter describing the required dependencies.

E.g.

/**
 * Dependencies for {@link RequesterImpl}
 */
export type RequesterImplDeps = {
    readonly networkState: NetworkState
    readonly fetch: Fetch
    readonly networkMetricsController: NetworkMetricsController
}

export class RequesterImpl {
    constructor(protected readonly deps: RequesterImplDeps) {}

    // ...
}

const requester = new Requester({
    networkState: new NetworkStateImpl(),
    fetch: new FetchImpl(),
})

There are two steps to dependency injection, creating a record of dependency factories, then creating a container that uses the factories to construct implementations.

When creating a record of dependency factories, the keys match the dependency properties, and the values are functions that produce a dependency. The factory functions may accept a single argument indicating any dependencies required for the implementation. Wrap the record in a call to validateDependencyFactories which is a compile-time-only check that ensures the dependency graph is sound. That is, there are no cyclic dependencies, missing dependencies, or incompatible types.

Example:

import { validateDependencyFactories } from '@amzn/vinyl-util'

class A {}

type BDependencies = {
    readonly a: A
}

class B {
    constructor(deps: BDependencies) {}
}

type CDependencies = {
    readonly a: A
    readonly b: B
}

class C {
    constructor(deps: CDependencies) {}
}

const factories = validateDependencyFactories({
    a: () => new A(),
    b: (deps: BDependencies) => new B(deps),
    c: (deps: CDependencies) => new C(deps),
} as const)

These factories will be used to create implementations. Use createDependenciesContainer to create a container that may be disposed and provides an object with getters to lazily-construct implementations from the provided factories.

import { createDependenciesContainer, createDisposer } from '@amzn/vinyl-util'

const { add, dispose } = createDisposer()

const deps = add(createDependenciesContainer(factories)).dependencies

deps.a // A {}
deps.b // B {}
deps.c // C {}

As a general guideline, when defining a dependency to be provided, the interface requested should be the minimum API needed, and disposal should be the responsibility of the creator.