Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c60af59bec | ||
|
|
b654a3b26f | ||
|
|
9bb3e28b34 |
7
deno.json
Normal file
7
deno.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"fmt": {
|
||||
"semiColons": true,
|
||||
"singleQuote": true,
|
||||
"useTabs": true
|
||||
}
|
||||
}
|
||||
38
deno.lock
generated
Normal file
38
deno.lock
generated
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"version": "3",
|
||||
"remote": {
|
||||
"https://deno.land/std@0.224.0/assert/_constants.ts": "a271e8ef5a573f1df8e822a6eb9d09df064ad66a4390f21b3e31f820a38e0975",
|
||||
"https://deno.land/std@0.224.0/assert/assert.ts": "09d30564c09de846855b7b071e62b5974b001bb72a4b797958fe0660e7849834",
|
||||
"https://deno.land/std@0.224.0/assert/assert_almost_equals.ts": "9e416114322012c9a21fa68e187637ce2d7df25bcbdbfd957cd639e65d3cf293",
|
||||
"https://deno.land/std@0.224.0/assert/assert_array_includes.ts": "14c5094471bc8e4a7895fc6aa5a184300d8a1879606574cb1cd715ef36a4a3c7",
|
||||
"https://deno.land/std@0.224.0/assert/assert_equals.ts": "3bbca947d85b9d374a108687b1a8ba3785a7850436b5a8930d81f34a32cb8c74",
|
||||
"https://deno.land/std@0.224.0/assert/assert_exists.ts": "43420cf7f956748ae6ed1230646567b3593cb7a36c5a5327269279c870c5ddfd",
|
||||
"https://deno.land/std@0.224.0/assert/assert_false.ts": "3e9be8e33275db00d952e9acb0cd29481a44fa0a4af6d37239ff58d79e8edeff",
|
||||
"https://deno.land/std@0.224.0/assert/assert_greater.ts": "5e57b201fd51b64ced36c828e3dfd773412c1a6120c1a5a99066c9b261974e46",
|
||||
"https://deno.land/std@0.224.0/assert/assert_greater_or_equal.ts": "9870030f997a08361b6f63400273c2fb1856f5db86c0c3852aab2a002e425c5b",
|
||||
"https://deno.land/std@0.224.0/assert/assert_instance_of.ts": "e22343c1fdcacfaea8f37784ad782683ec1cf599ae9b1b618954e9c22f376f2c",
|
||||
"https://deno.land/std@0.224.0/assert/assert_is_error.ts": "f856b3bc978a7aa6a601f3fec6603491ab6255118afa6baa84b04426dd3cc491",
|
||||
"https://deno.land/std@0.224.0/assert/assert_less.ts": "60b61e13a1982865a72726a5fa86c24fad7eb27c3c08b13883fb68882b307f68",
|
||||
"https://deno.land/std@0.224.0/assert/assert_less_or_equal.ts": "d2c84e17faba4afe085e6c9123a63395accf4f9e00150db899c46e67420e0ec3",
|
||||
"https://deno.land/std@0.224.0/assert/assert_match.ts": "ace1710dd3b2811c391946954234b5da910c5665aed817943d086d4d4871a8b7",
|
||||
"https://deno.land/std@0.224.0/assert/assert_not_equals.ts": "78d45dd46133d76ce624b2c6c09392f6110f0df9b73f911d20208a68dee2ef29",
|
||||
"https://deno.land/std@0.224.0/assert/assert_not_instance_of.ts": "3434a669b4d20cdcc5359779301a0588f941ffdc2ad68803c31eabdb4890cf7a",
|
||||
"https://deno.land/std@0.224.0/assert/assert_not_match.ts": "df30417240aa2d35b1ea44df7e541991348a063d9ee823430e0b58079a72242a",
|
||||
"https://deno.land/std@0.224.0/assert/assert_not_strict_equals.ts": "37f73880bd672709373d6dc2c5f148691119bed161f3020fff3548a0496f71b8",
|
||||
"https://deno.land/std@0.224.0/assert/assert_object_match.ts": "411450fd194fdaabc0089ae68f916b545a49d7b7e6d0026e84a54c9e7eed2693",
|
||||
"https://deno.land/std@0.224.0/assert/assert_rejects.ts": "4bee1d6d565a5b623146a14668da8f9eb1f026a4f338bbf92b37e43e0aa53c31",
|
||||
"https://deno.land/std@0.224.0/assert/assert_strict_equals.ts": "b4f45f0fd2e54d9029171876bd0b42dd9ed0efd8f853ab92a3f50127acfa54f5",
|
||||
"https://deno.land/std@0.224.0/assert/assert_string_includes.ts": "496b9ecad84deab72c8718735373feb6cdaa071eb91a98206f6f3cb4285e71b8",
|
||||
"https://deno.land/std@0.224.0/assert/assert_throws.ts": "c6508b2879d465898dab2798009299867e67c570d7d34c90a2d235e4553906eb",
|
||||
"https://deno.land/std@0.224.0/assert/assertion_error.ts": "ba8752bd27ebc51f723702fac2f54d3e94447598f54264a6653d6413738a8917",
|
||||
"https://deno.land/std@0.224.0/assert/equal.ts": "bddf07bb5fc718e10bb72d5dc2c36c1ce5a8bdd3b647069b6319e07af181ac47",
|
||||
"https://deno.land/std@0.224.0/assert/fail.ts": "0eba674ffb47dff083f02ced76d5130460bff1a9a68c6514ebe0cdea4abadb68",
|
||||
"https://deno.land/std@0.224.0/assert/mod.ts": "48b8cb8a619ea0b7958ad7ee9376500fe902284bb36f0e32c598c3dc34cbd6f3",
|
||||
"https://deno.land/std@0.224.0/assert/unimplemented.ts": "8c55a5793e9147b4f1ef68cd66496b7d5ba7a9e7ca30c6da070c1a58da723d73",
|
||||
"https://deno.land/std@0.224.0/assert/unreachable.ts": "5ae3dbf63ef988615b93eb08d395dda771c96546565f9e521ed86f6510c29e19",
|
||||
"https://deno.land/std@0.224.0/fmt/colors.ts": "508563c0659dd7198ba4bbf87e97f654af3c34eb56ba790260f252ad8012e1c5",
|
||||
"https://deno.land/std@0.224.0/internal/diff.ts": "6234a4b493ebe65dc67a18a0eb97ef683626a1166a1906232ce186ae9f65f4e6",
|
||||
"https://deno.land/std@0.224.0/internal/format.ts": "0a98ee226fd3d43450245b1844b47003419d34d210fa989900861c79820d21c2",
|
||||
"https://deno.land/std@0.224.0/internal/mod.ts": "534125398c8e7426183e12dc255bb635d94e06d0f93c60a297723abe69d3b22e"
|
||||
}
|
||||
}
|
||||
@@ -1,77 +1,77 @@
|
||||
import {
|
||||
assertEquals,
|
||||
assertRejects,
|
||||
assertThrows,
|
||||
} from "https://deno.land/std@0.224.0/assert/mod.ts";
|
||||
import { DependencyManager } from "./dependency-manager.ts";
|
||||
assertEquals,
|
||||
assertThrows,
|
||||
} from 'https://deno.land/std@0.224.0/assert/mod.ts';
|
||||
|
||||
// Test case for registering a module
|
||||
Deno.test("DependencyManager: register a module", () => {
|
||||
const manager = new DependencyManager();
|
||||
const provider = () => "dependency";
|
||||
manager.register("identifier", provider);
|
||||
assertEquals(
|
||||
manager.resolve("identifier"),
|
||||
Promise.resolve("dependency"),
|
||||
);
|
||||
import { Identifier, Manager, Provider } from './dependency-manager.ts';
|
||||
|
||||
Deno.test('provide: should throw error when an identifier is reused', () => {
|
||||
const value = 'test';
|
||||
const identifier = Symbol() as Identifier<typeof value>;
|
||||
|
||||
const manager = new Manager();
|
||||
manager.register(identifier, () => value);
|
||||
|
||||
assertThrows(
|
||||
() => manager.register(identifier, () => value),
|
||||
Error,
|
||||
);
|
||||
});
|
||||
|
||||
// Test case for resolving a module
|
||||
Deno.test("DependencyManager: resolve a module", async () => {
|
||||
const manager = new DependencyManager();
|
||||
const provider = () => "dependency";
|
||||
manager.register("identifier", provider);
|
||||
const result = await manager.resolve("identifier");
|
||||
assertEquals(result, "dependency");
|
||||
Deno.test('provide: should throw error when resolving a non-registered dependency', () => {
|
||||
const identifier = Symbol();
|
||||
const manager = new Manager();
|
||||
|
||||
assertThrows(
|
||||
() => manager.inject(identifier),
|
||||
Error,
|
||||
);
|
||||
});
|
||||
|
||||
// Test case for resolving an async module
|
||||
Deno.test("DependencyManager: resolve an async module", async () => {
|
||||
const manager = new DependencyManager();
|
||||
const provider = () => {
|
||||
return new Promise((resolve) =>
|
||||
setTimeout(() => resolve("dependency"), 100)
|
||||
);
|
||||
};
|
||||
manager.register("identifier", provider);
|
||||
const result = await manager.resolve("identifier");
|
||||
assertEquals(result, "dependency");
|
||||
Deno.test('inject: should return the provided value (stand-alone injectable)', () => {
|
||||
const value = 'foo';
|
||||
const identifier = Symbol() as Identifier<typeof value>;
|
||||
|
||||
const manager = new Manager();
|
||||
manager.register(identifier, () => value);
|
||||
|
||||
const resolved = manager.inject(identifier);
|
||||
assertEquals(resolved, 'foo');
|
||||
});
|
||||
|
||||
// Test case for handling circular dependencies
|
||||
Deno.test("DependencyManager: circular dependency detection", async () => {
|
||||
const manager = new DependencyManager();
|
||||
const providerA = async () => await manager.resolve("B");
|
||||
const providerB = async () => await manager.resolve("A");
|
||||
Deno.test('inject: should return the expected value (injectable with dependencies)', () => {
|
||||
const providerA = () => 'a';
|
||||
const identifierA = Symbol() as Identifier<ReturnType<typeof providerA>>;
|
||||
|
||||
manager.register("A", providerA);
|
||||
manager.register("B", providerB);
|
||||
const providerB = (m: Manager) => {
|
||||
const a = manager.inject(identifierA);
|
||||
|
||||
await assertRejects(
|
||||
() => manager.resolve("A"),
|
||||
Error,
|
||||
"Circular dependency detected for module A.",
|
||||
);
|
||||
return `-${a}-`;
|
||||
};
|
||||
const identifierB = Symbol() as Identifier<ReturnType<typeof providerB>>;
|
||||
|
||||
const manager = new Manager();
|
||||
manager.register(identifierA, providerA);
|
||||
manager.register(identifierB, providerB);
|
||||
|
||||
const b = manager.inject(identifierB);
|
||||
|
||||
assertEquals(b, '-a-');
|
||||
});
|
||||
|
||||
// Test case for handling duplicate module registration
|
||||
Deno.test("DependencyManager: duplicate module registration", () => {
|
||||
const manager = new DependencyManager();
|
||||
const provider = () => "dependency";
|
||||
manager.register("identifier", provider);
|
||||
assertThrows(
|
||||
() => manager.register("identifier", provider),
|
||||
Error,
|
||||
"Module identifier is already registered.",
|
||||
);
|
||||
});
|
||||
Deno.test('inject: should throw error when detecting a circular dependency', () => {
|
||||
const identifierA = Symbol();
|
||||
const identifierB = Symbol();
|
||||
|
||||
// Test case for handling unresolved modules
|
||||
Deno.test("DependencyManager: resolve an unregistered module", async () => {
|
||||
const manager = new DependencyManager();
|
||||
await assertRejects(
|
||||
() => manager.resolve("unregistered-identifier"),
|
||||
Error,
|
||||
"Module unregistered-identifier is not registered.",
|
||||
);
|
||||
const providerA = (manager: Manager) => manager.inject(identifierB);
|
||||
const providerB = (manager: Manager) => manager.inject(identifierA);
|
||||
|
||||
const manager = new Manager();
|
||||
manager.register(identifierA, providerA);
|
||||
manager.register(identifierB, providerB);
|
||||
|
||||
assertThrows(
|
||||
() => manager.inject(identifierA),
|
||||
Error,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,75 +1,38 @@
|
||||
type Provider<T> = (manager: DependencyManager) => Promise<T> | T;
|
||||
export type Provider<T = unknown> = (manager: Manager) => T;
|
||||
export interface Identifier<T = unknown> extends Symbol {}
|
||||
|
||||
// deno-lint-ignore no-explicit-any
|
||||
type HasConstructor<T> = T extends new (...args: any[]) => unknown ? T : never;
|
||||
// deno-lint-ignore no-explicit-any
|
||||
type ConstructorInstance<T> = T extends new (...args: any[]) => infer I ? I
|
||||
: never;
|
||||
export class Manager {
|
||||
private providers = new Map<Identifier, Provider>();
|
||||
private instances = new Map<Identifier, unknown>();
|
||||
private resolving = new Set<Identifier>();
|
||||
|
||||
// deno-lint-ignore no-explicit-any
|
||||
type ModuleIdentifier = any;
|
||||
register<T>(identifier: Identifier<T>, provider: Provider<T>) {
|
||||
if (this.providers.has(identifier)) {
|
||||
throw new Error('This identifier is already used');
|
||||
}
|
||||
|
||||
type Modules = Map<ModuleIdentifier, {
|
||||
// deno-lint-ignore no-explicit-any
|
||||
provider: Provider<any>;
|
||||
// deno-lint-ignore no-explicit-any
|
||||
instance?: any;
|
||||
}>;
|
||||
this.providers.set(identifier, provider);
|
||||
}
|
||||
|
||||
export class DependencyManager {
|
||||
private modules: Modules = new Map();
|
||||
private resolving: Set<ModuleIdentifier> = new Set();
|
||||
inject<T>(identifier: Identifier<T>): T {
|
||||
if (!this.providers.has(identifier)) {
|
||||
throw new Error('This identifier has not been registred');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the given provider to be used to resolve the dependency identified by the given identifier.
|
||||
*
|
||||
* Throw an error if the identifier is already used.
|
||||
*
|
||||
* @param identifier the identifier that will be used to ask the manager for the resolved dependency.
|
||||
* @param provider a function that receive the dependency manager as param and return the dependency. Can be asynchronous.
|
||||
*/
|
||||
register<T>(identifier: ModuleIdentifier, provider: Provider<T>): void {
|
||||
if (this.modules.has(identifier)) {
|
||||
throw new Error(`Module ${identifier} is already registered.`);
|
||||
}
|
||||
this.modules.set(identifier, { provider });
|
||||
}
|
||||
if (this.instances.has(identifier)) {
|
||||
return this.instances.get(identifier) as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the dependency matching the given identifier.
|
||||
* If the dependency has not been resolved yet, resolve it first.
|
||||
*
|
||||
* Throw an error if:
|
||||
* - no provider has been registred with that identifier
|
||||
* - a circular dependency is detected
|
||||
*
|
||||
* @param identifier the identifier used to register the provider
|
||||
*/
|
||||
async resolve<T>(
|
||||
identifier: HasConstructor<T>,
|
||||
): Promise<ConstructorInstance<T>>;
|
||||
async resolve<T>(identifier: ModuleIdentifier): Promise<T>;
|
||||
async resolve(identifier: ModuleIdentifier) {
|
||||
const module = this.modules.get(identifier);
|
||||
if (!module) {
|
||||
throw new Error(`Module ${identifier} is not registered.`);
|
||||
}
|
||||
if (this.resolving.has(identifier)) {
|
||||
throw new Error('Circular dependency detected');
|
||||
}
|
||||
|
||||
if (module.instance) {
|
||||
return module.instance;
|
||||
}
|
||||
this.resolving.add(identifier);
|
||||
const declaration = this.providers.get(identifier) as Provider<T>;
|
||||
const instance = declaration(this);
|
||||
this.instances.set(identifier, instance);
|
||||
this.resolving.delete(identifier);
|
||||
|
||||
if (this.resolving.has(identifier)) {
|
||||
throw new Error(`Circular dependency detected for module ${identifier}.`);
|
||||
}
|
||||
|
||||
this.resolving.add(identifier);
|
||||
|
||||
try {
|
||||
module.instance = await module.provider(this);
|
||||
return module.instance;
|
||||
} finally {
|
||||
this.resolving.delete(identifier);
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
6
mod.ts
6
mod.ts
@@ -1 +1,5 @@
|
||||
export * from "./dependency-manager.ts";
|
||||
export {
|
||||
type Identifier as DependencyIdentifier,
|
||||
Manager as DependencyManager,
|
||||
type Provider as DependencyProvider,
|
||||
} from './dependency-manager.ts';
|
||||
|
||||
73
readme.md
73
readme.md
@@ -5,64 +5,71 @@ This lib provides a simplistic dependency manager.
|
||||
## Features
|
||||
|
||||
- Resolves dependencies only when they are needed
|
||||
- Detect circular dependency
|
||||
- Accept any valid `Map` key as dependency identifier
|
||||
- Infer the dependency type from the dependency identifier if the later is a class
|
||||
- Detects circular dependency
|
||||
- Strong type support
|
||||
|
||||
## Usage
|
||||
The [previous major release](https://git.chpt.dev/Chappatte/Typescript-Dependency-Manager/src/tag/v1.1.1) used to allow asynchronous dependencies and the use of class as identifier, but those features were replaced by the `Symbol` identifier to keep the lib dead simple.
|
||||
|
||||
1. Create a manager:
|
||||
## Basic usage
|
||||
|
||||
```typescript
|
||||
import { DependencyManager } from 'dependency-manager/mod.ts'
|
||||
|
||||
const manager = new DependencyManager()
|
||||
// 1. Create a manager:
|
||||
const manager = new DependencyManager();
|
||||
|
||||
// 2. Create a dependency (a provider and an identifier):
|
||||
const value = 'foo';
|
||||
const provider = () => value;
|
||||
const identifier = Symbol() as DependencyIdentifier<typeof value>;
|
||||
|
||||
// 3. Register the dependency:
|
||||
manager.register(identifier, provider);
|
||||
|
||||
// 4. Anywhere in your code, inject the resolved dependency value:
|
||||
const value = await manager.inject(identifier);
|
||||
```
|
||||
|
||||
2. Register dependencies by giving the manager an identifier and a provider:
|
||||
## Identifier
|
||||
|
||||
```typescript
|
||||
manager.register('dependency-identifier', () => 'value');
|
||||
```
|
||||
An identifier is a uniq value used by the dependency manager to store and retrieve the dependency.
|
||||
|
||||
3. Get the dependency by giving the manager its identifier (always asynchrone):
|
||||
|
||||
```typescript
|
||||
const value = await manager.resolve('dependency-identifier');
|
||||
```
|
||||
The `DependencyIdentifier` type allow the return value of the `inject` method to be fully typed,
|
||||
|
||||
## Providers
|
||||
|
||||
Providers are functions that are called when resolving the dependency for the first time.
|
||||
They receive the dependency manager as parameter, and should return the dependency value, or a promise that resolves to it.
|
||||
A dependency is rarely just a simple value like in the basic usage code and often rely on other dependencies.
|
||||
|
||||
Example of the registration of a dependency which provider uses another dependency:
|
||||
That's why the `register` method takes a function (that we call the dependency `provider`) who's role is to build the dependency.
|
||||
|
||||
That function receive a uniq parameter: The dependency manager.
|
||||
That way it is possible to inject dependencies inside it:
|
||||
|
||||
```typescript
|
||||
async function provider(manager: DependencyManager) {
|
||||
const value = await manager.resolve('dependencyIdentifier');
|
||||
return `The value is: ${valueA}`;
|
||||
function providerA() {
|
||||
return 'foo'
|
||||
}
|
||||
manager.register('other-identifier', provider);
|
||||
```
|
||||
const identifierA = Symbol() as DependencyIdentifier<ReturnType<typeof providerA>>;
|
||||
|
||||
## Identifiers and typing
|
||||
function providerB(manager: DependencyManager) {
|
||||
const a = manager.inject(identifierA);
|
||||
return `The value is: ${a}`;
|
||||
}
|
||||
const identifierB = Symbol() as DependencyIdentifier<ReturnType<typeof providerB>>;
|
||||
|
||||
Any valid `Map` key can be used as identifier, but using a class allow the return value of the `resolve` method to be automatically typed:
|
||||
manager.register(identifierA, providerA);
|
||||
manager.register(identifierB, providerB);
|
||||
|
||||
```typescript
|
||||
class MyDependency {}
|
||||
// ...
|
||||
|
||||
const a = await manager.resolve(MyDependecy); //< `a` is of type `MyDependency`
|
||||
const b = await manager.resolve('dependency-identifier'); //< `b` is of type `unknown`
|
||||
const c = await manager.resolve<number>('dependency-identifier'); //< `c` is of type `number`
|
||||
const b = manager.inject(identifierB); // "The value is: foo"
|
||||
```
|
||||
|
||||
## Errors
|
||||
|
||||
The `register` method throw an error if:
|
||||
- the name is already used.
|
||||
- the dependency is already registred.
|
||||
|
||||
The `resolve` method throw an error if:
|
||||
- the name doesn't exist (if no module has been registred with the given name).
|
||||
The `inject` method throw an error if:
|
||||
- the dependency is not registred.
|
||||
- a circular dependency is detected
|
||||
Reference in New Issue
Block a user