-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* update ci config * child container and init testing * simple provider resolution * add support for transient providers * add error case * resolve from parents * add init lifecycle interface * add changelogs * inital readme and rename packages * readme and changelogs * allow warnings for tests
- Loading branch information
Showing
14 changed files
with
422 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
10 changes: 10 additions & 0 deletions
10
common/changes/@dtty/simpldi/add-simpldi_2024-11-11-16-06.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
{ | ||
"changes": [ | ||
{ | ||
"packageName": "@dtty/simpldi", | ||
"comment": "Initial release", | ||
"type": "minor" | ||
} | ||
], | ||
"packageName": "@dtty/simpldi" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
# @dtty/simpldi | ||
|
||
A simple dependency injection library for typescript projects. | ||
|
||
## Installation | ||
|
||
This library requires the `reflect-metadata` package to function properly. | ||
Add the line `import 'reflect-metadata';` to the top of you entry file (e.g. index.js). | ||
|
||
```sh | ||
npm i @dtty/simpldi reflect-metadata | ||
``` | ||
|
||
## Usage | ||
|
||
Unlike many other dependency injection libraries for Typescript projects, `@dtty/simpldi` requires developers to manage the container lifecycle themselvs. | ||
While some may consider this a burden, it ultimately allows for far more flexible configurations and a simpler API to engage with. | ||
|
||
```ts | ||
// Basic usage | ||
|
||
const rootContainer = new Container(); | ||
|
||
// Add a simple provider | ||
rootContainer.addProvider(simpleToken, SimpleProviderClass); | ||
|
||
// Add a transient provider | ||
rootContainer.addProvider(transientToken, TransientProviderClass, { | ||
mode: ProviderMode.TRANSIENT, | ||
}); | ||
|
||
// Fetch a provider | ||
const instance = await rootContainer.resolveProvider(simpleToken); | ||
``` | ||
|
||
### Tokens | ||
|
||
Tokens in `@dtty/simpldi` are used in place of magic strings for dependency resolution. | ||
This enables stricter type checking when manually resolving dependencies. | ||
|
||
```ts | ||
// Create a token | ||
const token = new Token<SimpleProviderClass>(); | ||
|
||
// Optionall, add a name to the token | ||
const tokenWithName = new Token<SimpleProviderClass>("My Name"); | ||
``` | ||
|
||
### Modes | ||
|
||
By default, all providers in the container are registered as singletons. | ||
This behavior can be optionally changed by setting `mode: ProviderMode.TRANSIENT` when registering a provider. | ||
Transient providers will be re-initialized each time they are resolved, ensuring all downstream providers recieve a fresh instance. | ||
|
||
### Nested Containers | ||
|
||
Providers are always scoped to their container but can depend on providers from a parent container. | ||
Creating a child container allows developers to scope specific providers to the lifetime of the container. for example the use of a child container for request-specific providers in a web server. | ||
|
||
```ts | ||
// Start with a root | ||
const rootContainer = new Container(); | ||
|
||
// And spawn a child | ||
const childContainer = rootContainer.createChildContainer(); | ||
``` | ||
|
||
### Lifecycle | ||
|
||
By default, all providers a lazy loaded into the container, meaning that provider instances are only created when the are needed. | ||
Lazy loading enables a faster application start time and ensures that only the relevant classes are created, cutting down on overhead. | ||
|
||
Below are ways to hook into the provider lifecycle. | ||
|
||
#### onProviderInit | ||
|
||
By implementing the `IOnProviderInit` interface, a provider adds a lifecycle method that is called whenever the provider instance is resolved in the container. | ||
This can be useful for bootstrapping common providers like database connections that require an asynchronous interaction after creation. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
import { Container } from "./container"; | ||
import { Inject } from "./inject"; | ||
import { ProviderNotFoundException } from "./provider-not-found.exception"; | ||
import { Token } from "./token"; | ||
import { IOnProviderInit, ProviderMode } from "./types"; | ||
|
||
describe("Container interactions", () => { | ||
let rootContainer: Container; | ||
class ProviderWithNoDeps { | ||
public readonly Id: string; | ||
constructor() { | ||
this.Id = (Math.random() * Date.now()).toString(); | ||
} | ||
} | ||
|
||
const noDepsToken = new Token<ProviderWithNoDeps>("No deps"); | ||
|
||
class ProviderWithOneDep extends ProviderWithNoDeps { | ||
constructor(@Inject(noDepsToken) public readonly dep: ProviderWithNoDeps) { | ||
super(); | ||
} | ||
} | ||
|
||
const oneDepToken = new Token<ProviderWithOneDep>("One Dep"); | ||
|
||
class ProviderWithMultipleDeps extends ProviderWithNoDeps { | ||
constructor( | ||
@Inject(noDepsToken) public readonly noDeps: ProviderWithNoDeps, | ||
@Inject(oneDepToken) public readonly oneDep: ProviderWithOneDep, | ||
) { | ||
super(); | ||
} | ||
} | ||
|
||
const mulipleDepToken = new Token<ProviderWithMultipleDeps>("many deps"); | ||
|
||
beforeEach(() => { | ||
rootContainer = new Container(); | ||
}); | ||
|
||
it("should create a child container linked to the root", () => { | ||
const child = rootContainer.createChildContainer(); | ||
expect(child["parent"]).toEqual(rootContainer); | ||
}); | ||
|
||
describe("Single layer provider lifecycle", () => { | ||
it("should resolve a single provider with no dependencies", async () => { | ||
rootContainer.addProvider(noDepsToken, ProviderWithNoDeps); | ||
const provider = await rootContainer.resolveProvider(noDepsToken); | ||
expect(provider).toBeInstanceOf(ProviderWithNoDeps); | ||
expect(provider.Id).not.toBeUndefined(); | ||
}); | ||
|
||
it("should always resolve the same provider instance by default", async () => { | ||
rootContainer.addProvider(noDepsToken, ProviderWithNoDeps); | ||
const provider1 = await rootContainer.resolveProvider(noDepsToken); | ||
const provider2 = await rootContainer.resolveProvider(noDepsToken); | ||
expect(provider1.Id).toEqual(provider2.Id); | ||
}); | ||
|
||
it("should resolve a single provider with a singleton dependency", async () => { | ||
rootContainer.addProvider(noDepsToken, ProviderWithNoDeps); | ||
rootContainer.addProvider(oneDepToken, ProviderWithOneDep); | ||
|
||
const provider = await rootContainer.resolveProvider(oneDepToken); | ||
expect(provider).toBeInstanceOf(ProviderWithOneDep); | ||
expect(provider.dep).toBeInstanceOf(ProviderWithNoDeps); | ||
expect(provider.Id).not.toEqual(provider.dep.Id); | ||
}); | ||
|
||
it("should resolve a single provider with a transient dependency", async () => { | ||
rootContainer.addProvider(oneDepToken, ProviderWithOneDep); | ||
rootContainer.addProvider(noDepsToken, ProviderWithNoDeps, { | ||
mode: ProviderMode.TRANSIENT, | ||
}); | ||
const provider1 = await rootContainer.resolveProvider(oneDepToken); | ||
const provider2 = await rootContainer.resolveProvider(noDepsToken); | ||
expect(provider1.dep.Id).not.toEqual(provider2.Id); | ||
}); | ||
|
||
it("should resolve a provider with multiple dependencies", async () => { | ||
rootContainer.addProvider(noDepsToken, ProviderWithNoDeps); | ||
rootContainer.addProvider(oneDepToken, ProviderWithOneDep); | ||
rootContainer.addProvider(mulipleDepToken, ProviderWithMultipleDeps); | ||
const provider = await rootContainer.resolveProvider(mulipleDepToken); | ||
expect(provider).toBeInstanceOf(ProviderWithMultipleDeps); | ||
expect(provider.noDeps.Id).toEqual(provider.oneDep.dep.Id); | ||
}); | ||
|
||
it("should throw an exception when resolving a missing provider", async () => { | ||
await expect(() => | ||
rootContainer.resolveProvider(noDepsToken), | ||
).rejects.toThrow(ProviderNotFoundException); | ||
}); | ||
}); | ||
|
||
describe("Multi layer provider lifecycle", () => { | ||
let childContainer: Container; | ||
|
||
beforeEach(() => { | ||
childContainer = rootContainer.createChildContainer(); | ||
}); | ||
|
||
it("should resolve a provider from the child using a parent dependency", async () => { | ||
rootContainer.addProvider(noDepsToken, ProviderWithNoDeps); | ||
childContainer.addProvider(oneDepToken, ProviderWithOneDep); | ||
const provider1 = await childContainer.resolveProvider(oneDepToken); | ||
const provider2 = await rootContainer.resolveProvider(noDepsToken); | ||
expect(provider1.dep.Id).toEqual(provider2.Id); | ||
}); | ||
|
||
it("should throw an exception when resolving a provider that depends on the child", async () => { | ||
childContainer.addProvider(noDepsToken, ProviderWithNoDeps); | ||
rootContainer.addProvider(oneDepToken, ProviderWithOneDep); | ||
await expect(() => | ||
childContainer.resolveProvider(oneDepToken), | ||
).rejects.toThrow(ProviderNotFoundException); | ||
}); | ||
}); | ||
|
||
describe("Provider lifecycle methods", () => { | ||
class InitAbleProvider | ||
extends ProviderWithNoDeps | ||
implements IOnProviderInit | ||
{ | ||
public isInit: boolean; | ||
|
||
constructor() { | ||
super(); | ||
this.isInit = false; | ||
} | ||
public onProviderInit(): void { | ||
this.isInit = true; | ||
} | ||
} | ||
|
||
const initToken = new Token<InitAbleProvider>(); | ||
|
||
it("should call onProviderInit if present when constructing a provider instance", async () => { | ||
rootContainer.addProvider(initToken, InitAbleProvider); | ||
const provider = await rootContainer.resolveProvider(initToken); | ||
expect(provider.isInit).toEqual(true); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.