-
-
Notifications
You must be signed in to change notification settings - Fork 150
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(ui): add dependency injection container (#1006)
- Implement `createInjectionContainer` for managing service lifecycles - Implement `provide` and `inject` for dependency injection - Support singleton and transient service lifecycles - Add unit tests for container functionality
- Loading branch information
Showing
3 changed files
with
168 additions
and
0 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
import { describe, expect, it } from 'vitest' | ||
import { createInjectionContainer } from './container' | ||
|
||
describe('InjectionContainer', () => { | ||
it('should inject a singleton service', () => { | ||
const container = createInjectionContainer() | ||
|
||
// Mock Service | ||
const myService = { | ||
getValue: () => 'test value', | ||
} | ||
|
||
// Provide service as singleton | ||
container.provide('myService', () => myService) | ||
|
||
// Inject service and validate | ||
const injectedService = container.inject('myService') | ||
expect(injectedService).toBe(myService) | ||
}) | ||
|
||
it('should inject a transient service', () => { | ||
const container = createInjectionContainer() | ||
|
||
// Provide service as transient | ||
container.provide('myService', () => ({ id: Math.random() }), [], { lifecycle: 'transient' }) | ||
|
||
// Inject service twice and expect different instances | ||
const service1 = container.inject('myService') | ||
const service2 = container.inject('myService') | ||
expect(service1).not.toBe(service2) | ||
}) | ||
|
||
it('should throw error when service is not provided', () => { | ||
const container = createInjectionContainer() | ||
|
||
// Try injecting a non-existent service | ||
expect(() => container.inject('nonExistentService')).toThrowError( | ||
'No provide for nonExistentService', | ||
) | ||
}) | ||
|
||
it('should inject services with dependencies', () => { | ||
type Dep1 = { getName: () => string } | ||
type Dep2 = { getName: () => string } | ||
type Deps = { dep1: Dep1; dep2: Dep2; myService: MyService } | ||
|
||
const container = createInjectionContainer<Deps>() | ||
|
||
// Mock dependencies | ||
const dep1: Dep1 = { getName: () => 'Dependency 1' } | ||
const dep2: Dep2 = { getName: () => 'Dependency 2' } | ||
|
||
// Provide dependencies | ||
container.provide('dep1', () => dep1) | ||
container.provide('dep2', () => dep2) | ||
|
||
// Mock service depending on dep1 and dep2 | ||
class MyService { | ||
constructor( | ||
public dep1: Dep1, | ||
public dep2: Dep2, | ||
) {} | ||
|
||
getDeps() { | ||
return [this.dep1.getName(), this.dep2.getName()] | ||
} | ||
} | ||
|
||
container.provide('myService', (dep1, dep2) => new MyService(dep1, dep2), [ | ||
'dep1', | ||
'dep2', | ||
] as const) | ||
|
||
// Inject service and validate dependencies | ||
const myService = container.inject('myService') | ||
expect(myService.getDeps()).toEqual(['Dependency 1', 'Dependency 2']) | ||
}) | ||
|
||
it('should inject singleton with dependencies only once', () => { | ||
const container = createInjectionContainer() | ||
|
||
// Mock dependencies | ||
const dep1 = { id: Math.random() } | ||
const dep2 = { id: Math.random() } | ||
|
||
// Provide dependencies as singletons | ||
container.provide('dep1', () => dep1) | ||
container.provide('dep2', () => dep2) | ||
|
||
// Provide service depending on dep1 and dep2 | ||
container.provide('myService', (dep1, dep2) => ({ dep1, dep2 }), ['dep1', 'dep2']) | ||
|
||
// Inject service and dependencies | ||
const myService1 = container.inject('myService') | ||
const myService2 = container.inject('myService') | ||
|
||
// Expect same instance for singleton | ||
expect(myService1).toBe(myService2) | ||
expect(myService1.dep1).toBe(dep1) | ||
expect(myService1.dep2).toBe(dep2) | ||
}) | ||
}) |
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,65 @@ | ||
type PrimitiveKey = string | number | symbol | ||
type Services = { [key: PrimitiveKey]: any } | ||
|
||
export type Constructor<T, S = Services, D extends readonly (keyof S)[] = any> = ( | ||
...args: { [K in keyof D]: D[K] extends keyof S ? S[D[K]] : never } | ||
) => T | ||
|
||
export type Lifecycle = 'transient' | 'singleton' | ||
|
||
export interface Provider<T, S = Services> { | ||
/** Implementation constructor */ | ||
impl: Constructor<T> | ||
|
||
/** Dependency constructors */ | ||
deps: readonly (keyof S)[] | ||
|
||
/** Lifecycle */ | ||
lifecycle: Lifecycle | ||
} | ||
|
||
export interface ProvideFuncOptions { | ||
/** Lifecycle (default: 'singleton') */ | ||
lifecycle?: Lifecycle | ||
} | ||
|
||
export interface DependencyContainer<S = Services> { | ||
provide<K extends keyof S, T extends S[K] = any, D extends readonly (keyof S)[] = any>( | ||
key: K, | ||
impl: Constructor<T, S, D>, | ||
deps?: D, | ||
opts?: ProvideFuncOptions, | ||
): void | ||
|
||
inject<T = undefined, K extends keyof S = any>(key: K): T extends undefined ? S[K] : T | ||
} | ||
|
||
export function createInjectionContainer<S = Services>(): DependencyContainer<S> { | ||
const providers = new Map<PrimitiveKey, Provider<any, S>>() | ||
const initializedDeps = new Map<PrimitiveKey, any>() | ||
|
||
const provide: DependencyContainer<S>['provide'] = (key, impl, deps, opts = {}) => { | ||
providers.set(key, { impl, deps: deps || [], lifecycle: opts.lifecycle || 'singleton' }) | ||
} | ||
|
||
const inject: DependencyContainer<S>['inject'] = (key) => { | ||
const provider = providers.get(key) | ||
if (!provider) { | ||
throw new Error(`No provide for ${String(key)}`) | ||
} | ||
|
||
if (provider.lifecycle === 'singleton' && initializedDeps.has(key)) { | ||
return initializedDeps.get(key) | ||
} | ||
|
||
const { impl, deps } = provider | ||
const params = deps.map((d) => inject(d)) | ||
const resolved = impl(...params) | ||
|
||
initializedDeps.set(key, resolved) | ||
|
||
return resolved | ||
} | ||
|
||
return { provide, inject } | ||
} |
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 @@ | ||
export * from './container' |