Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc31065348 | ||
|
|
777d8605a0 | ||
|
|
bd7d2aae65 |
77
dependency-manager.test.ts
Normal file
77
dependency-manager.test.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import {
|
||||
assertEquals,
|
||||
assertRejects,
|
||||
assertThrows,
|
||||
} from "https://deno.land/std@0.224.0/assert/mod.ts";
|
||||
import { DependencyManager } from "./dependency-manager.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"),
|
||||
);
|
||||
});
|
||||
|
||||
// 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");
|
||||
});
|
||||
|
||||
// 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");
|
||||
});
|
||||
|
||||
// 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");
|
||||
|
||||
manager.register("A", providerA);
|
||||
manager.register("B", providerB);
|
||||
|
||||
await assertRejects(
|
||||
() => manager.resolve("A"),
|
||||
Error,
|
||||
"Circular dependency detected for module 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.",
|
||||
);
|
||||
});
|
||||
|
||||
// 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.",
|
||||
);
|
||||
});
|
||||
@@ -1,44 +1,69 @@
|
||||
type Provider<T> = (manager: DependencyManager) => Promise<T> | T;
|
||||
|
||||
type Modules = Map<string, {
|
||||
type ClassType<T> = new () => T;
|
||||
|
||||
// deno-lint-ignore no-explicit-any
|
||||
type ModuleIdentifier = any;
|
||||
|
||||
type Modules = Map<ModuleIdentifier, {
|
||||
// deno-lint-ignore no-explicit-any
|
||||
provider: Provider<any>;
|
||||
// deno-lint-ignore no-explicit-any
|
||||
instance?: any;
|
||||
}>;
|
||||
|
||||
export class DependencyManager {
|
||||
private modules: Modules = new Map();
|
||||
private resolving: Set<string> = new Set();
|
||||
private resolving: Set<ModuleIdentifier> = new Set();
|
||||
|
||||
// Enregistrer un module avec son initialisateur
|
||||
register<T>(name: string, provider: Provider<T>): void {
|
||||
if (this.modules.has(name)) {
|
||||
throw new Error(`Module ${name} is already registered.`);
|
||||
/**
|
||||
* 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(name, { provider });
|
||||
this.modules.set(identifier, { provider });
|
||||
}
|
||||
|
||||
// Résoudre un module et ses dépendances
|
||||
async resolve<T>(name: string): Promise<T> {
|
||||
const module = this.modules.get(name);
|
||||
/**
|
||||
* 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: ClassType<T>): Promise<T>;
|
||||
async resolve<T>(identifier: ModuleIdentifier): Promise<T>;
|
||||
async resolve(identifier: ModuleIdentifier) {
|
||||
const module = this.modules.get(identifier);
|
||||
if (!module) {
|
||||
throw new Error(`Module ${name} is not registered.`);
|
||||
throw new Error(`Module ${identifier} is not registered.`);
|
||||
}
|
||||
|
||||
if (module.instance) {
|
||||
return module.instance;
|
||||
}
|
||||
|
||||
if (this.resolving.has(name)) {
|
||||
throw new Error(`Circular dependency detected for module ${name}.`);
|
||||
if (this.resolving.has(identifier)) {
|
||||
throw new Error(`Circular dependency detected for module ${identifier}.`);
|
||||
}
|
||||
|
||||
this.resolving.add(name);
|
||||
this.resolving.add(identifier);
|
||||
|
||||
try {
|
||||
module.instance = await module.provider(this);
|
||||
return module.instance;
|
||||
} finally {
|
||||
this.resolving.delete(name);
|
||||
this.resolving.delete(identifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
85
readme.md
85
readme.md
@@ -2,14 +2,16 @@
|
||||
|
||||
This lib provides a simplistic dependency manager.
|
||||
|
||||
## Features and limitations
|
||||
## Features
|
||||
|
||||
- It resolves dependencies only when they are needed
|
||||
- It detect circular dependency
|
||||
- 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
|
||||
|
||||
## Usage
|
||||
|
||||
Create a manager:
|
||||
1. Create a manager:
|
||||
|
||||
```typescript
|
||||
import { DependencyManager } from 'dependency-manager/mod.ts'
|
||||
@@ -17,48 +19,55 @@ import { DependencyManager } from 'dependency-manager/mod.ts'
|
||||
const manager = new DependencyManager()
|
||||
```
|
||||
|
||||
Create providers for your dependencies.
|
||||
A provider is a function that is called when attempting to retrieve the dependency for the first time.
|
||||
It receive the dependency manager, and return the dependency value, or a promise that resolves to it.
|
||||
2. Register dependencies by giving the manager an identifier and a provider:
|
||||
|
||||
```typescript
|
||||
// dependency-free provider
|
||||
function loggerProvider() {
|
||||
return console.log
|
||||
manager.register('dependency-identifier', () => 'value');
|
||||
```
|
||||
|
||||
3. Get the dependency by giving the manager its identifier (always asynchrone):
|
||||
|
||||
```typescript
|
||||
const value = await manager.resolve('dependency-identifier');
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
Example of the registration of a dependency which provider uses another dependency:
|
||||
|
||||
```typescript
|
||||
async function provider(manager: DependencyManager) {
|
||||
const value = await manager.resolve('dependencyIdentifier');
|
||||
return `The value is: ${valueA}`;
|
||||
}
|
||||
type Logger = ReturnType<typeof loggerProvider>;
|
||||
manager.register('other-identifier', provider);
|
||||
```
|
||||
|
||||
// provider that need the 'logger' dependency
|
||||
async function fooProvider(manager: DependencyManager) {
|
||||
const logger = await manager.resolve<Logger>('logger');
|
||||
## Identifiers and typing
|
||||
|
||||
return {
|
||||
bar: () => logger('bar'),
|
||||
baz: () => logger('baz'),
|
||||
}
|
||||
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:
|
||||
|
||||
```typescript
|
||||
class MyDependency {}
|
||||
|
||||
manager.register(MyDependency, () => new MyDependency());
|
||||
const a = await manager.resolve(MyDependecy); //< `a` is of type `MyDependency`
|
||||
```
|
||||
|
||||
Otherwise you can provide the type as type argument:
|
||||
|
||||
```typescript
|
||||
function provider() {
|
||||
return 'foo'
|
||||
}
|
||||
type Foo = Awaited<ReturnType<typeof fooProvider>>;
|
||||
manager.register('my-dependency-identifier', provider);
|
||||
const a = await manager.resolve('my-dependency-identifier'); //< `a` is of type `unknown`
|
||||
const b = await manager.resolve<string>('my-dependency-identifier'); //< `b` is of type `string`
|
||||
```
|
||||
|
||||
Register the dependency by passing the `register` method its name and provider:
|
||||
|
||||
```typescript
|
||||
manager.register('logger', loggerProvider);
|
||||
manager.register('foo', fooProvider);
|
||||
```
|
||||
|
||||
Retrieve dependencies by resolving them (always asynchrone, even if the provider was synchrone):
|
||||
|
||||
```typescript
|
||||
const foo = await manager.resolve<Foo>('foo');
|
||||
foo.bar();
|
||||
foo.baz();
|
||||
```
|
||||
|
||||
## Typing
|
||||
|
||||
Types should be explicitly defined when resolving a dependency.
|
||||
|
||||
## Errors
|
||||
|
||||
The `register` method throw an error if:
|
||||
|
||||
Reference in New Issue
Block a user