Skip to content

Commit 60a43ff

Browse files
committed
refactor: enhance dependency injection system with improved type definitions and async resolution
1 parent 0ee4f16 commit 60a43ff

File tree

5 files changed

+87
-56
lines changed

5 files changed

+87
-56
lines changed

src/decorators/Controller.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ export function Controller(path: string) {
66
return <T extends Constructor>(target: T) => {
77
controllerMetadata.set(target, path);
88
// Auto-register the controller as a singleton
9-
Container.register(target, () => new (target as any)(), "singleton");
9+
Container.register(target, () => new target(), "singleton");
1010
};
1111
}

src/decorators/Inject.ts

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,57 @@
11
import { Container } from "@/di/Container";
2+
import type { Constructor, Lifetime } from "@/types";
23
import { injectionMetadata } from "@/utils/meta-data";
34

4-
5+
type InjectableOptions = {
6+
lifetime?: Lifetime;
7+
token?: object; // For interface-based DI
8+
};
59

610
export function Inject(token: unknown) {
711
return (target: object, _: string | symbol, parameterIndex: number) => {
8-
const metadata = injectionMetadata.get(target.constructor) ?? [];
12+
const metadata = injectionMetadata.get(target) ?? [];
913
metadata[parameterIndex] = token;
10-
injectionMetadata.set(target.constructor, metadata);
14+
injectionMetadata.set(target, metadata);
1115
};
1216
}
1317

14-
export function Injectable() {
15-
return (constructor: new (...args: any[]) => any) => {
16-
Container.register(constructor, () => createInstance(constructor));
18+
export function Injectable(options?: InjectableOptions) {
19+
return (target: any) => {
20+
const token = options?.token ?? target;
21+
const lifetime = options?.lifetime ?? "singleton";
22+
23+
// Register the class in the container
24+
Container.register(
25+
token,
26+
() => {
27+
// Use the existing createInstance function for dependency resolution
28+
return createInstance(target);
29+
},
30+
lifetime
31+
);
32+
33+
// Store original constructor for potential edge cases
34+
target.__originalConstructor = target.prototype.constructor;
35+
1736
};
1837
}
1938

2039

21-
export function createInstance<T>(constructor: new (...args: any[]) => T): T {
22-
const metadata = injectionMetadata.get(constructor) ?? [];
40+
function createInstance<T>(constructor: Constructor<T>): T {
41+
// Use original constructor if available
42+
const ctor = constructor.prototype?.__originalConstructor || constructor;
43+
const metadata: any[] = injectionMetadata.get(ctor) ?? [];
44+
2345
const args = metadata.map((token) => {
2446
try {
2547
return Container.resolve(token);
2648
} catch (error) {
27-
if (error instanceof Error && error.message.startsWith('Circular dependency')) {
28-
// Enhance error message with class name
29-
throw new Error(
30-
`${error.message} in ${constructor.name} constructor`
31-
);
49+
if (error instanceof Error) {
50+
throw new Error(`Error resolving ${token}: ${error.message}`);
3251
}
3352
throw error;
3453
}
3554
});
36-
return new constructor(...args);
55+
56+
return new ctor(...args);
3757
}

src/di/Container.ts

Lines changed: 42 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,58 @@
1-
type Provider<T = any> = {
2-
factory: () => T;
3-
lifetime: "singleton" | "transient";
4-
};
1+
import type { Factory, Lifetime } from "@/types";
2+
3+
interface Dependency<T = any> {
4+
lifetime: Lifetime;
5+
factory: Factory<T>;
6+
instance?: T;
7+
}
58

69
export class Container {
7-
private static registry = new Map<unknown, Provider>();
8-
private static instances = new WeakMap<object, unknown>();
9-
private static resolutionStack: unknown[] = []; // Track resolution path for circular dependencies
10+
private static dependencies = new WeakMap<object, Dependency>();
1011

1112
static register<T>(
12-
token: unknown,
13-
factory: () => T,
14-
lifetime: "singleton" | "transient" = "singleton"
13+
token: object,
14+
factory: Factory<T>,
15+
lifetime: Lifetime = "singleton"
1516
) {
16-
this.registry.set(token, { factory, lifetime });
17+
this.dependencies.set(token, { factory, lifetime });
1718
}
1819

19-
static resolve<T>(token: T): T {
20-
// Detect circular dependencies
21-
if (this.resolutionStack.includes(token)) {
22-
const cycleStart = this.resolutionStack.indexOf(token);
23-
const cyclePath = [
24-
...this.resolutionStack.slice(cycleStart),
25-
token
26-
].map(t => this.getTokenName(t)).join(' -> ');
27-
28-
throw new Error(`Circular dependency detected: ${cyclePath}`);
29-
}
30-
20+
static resolve<T extends object>(token: T): T {
3121
try {
32-
this.resolutionStack.push(token);
33-
const provider = this.registry.get(token);
34-
if (!provider) { throw new Error(`No provider for token: ${String(token)}`); };
35-
36-
// Handle singleton lifetime
37-
if (provider.lifetime === "singleton") {
38-
if (!this.instances.has(token as object)) {
39-
this.instances.set(token as object, provider.factory());
22+
const dep = this.dependencies.get(token);
23+
if (!dep) {
24+
throw new Error(`Dependency ${token} not registered`);
25+
}
26+
27+
if (dep.lifetime === "singleton") {
28+
if (!dep.instance) {
29+
dep.instance = dep.factory();
4030
}
41-
return this.instances.get(token as object) as T;
31+
return dep.instance;
4232
}
4333

44-
// Transient lifetime
45-
return provider.factory() as T;
46-
} finally {
47-
// Clean up stack even if errors occur
48-
this.resolutionStack.pop();
34+
return dep.factory();
35+
} catch (error) {
36+
if (error instanceof Error) {
37+
throw new Error(`Error resolving ${token}: ${error.message}`);
38+
}
39+
throw error;
4940
}
5041
}
5142

52-
private static getTokenName(token: unknown): string {
53-
if (typeof token === 'function') { return token.name || 'AnonymousClass'; };
54-
if (typeof token === 'symbol') { return token.toString(); };
55-
return String(token);
43+
static async resolveAsync<T>(token: object): Promise<T> {
44+
const dep = this.dependencies.get(token);
45+
if (!dep) {
46+
throw new Error(`Dependency ${token} not found`);
47+
};
48+
49+
if (dep.lifetime === "singleton") {
50+
if (!dep.instance) {
51+
dep.instance = await dep.factory();
52+
}
53+
return dep.instance;
54+
}
55+
56+
return await dep.factory();
5657
}
5758
}

src/exp.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
class Cat { }
2+
3+
const cat = new Cat();
4+
5+
console.log({
6+
type: typeof Cat,
7+
instance: typeof cat,
8+
});

src/types/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
export type Constructor<T = {}> = new (...args: any[]) => T;
2+
export type Factory<T> = () => T;
3+
export type Lifetime = "singleton" | "transient";
24

35

46
export type ServerOptions = {

0 commit comments

Comments
 (0)