From 96a0cfbb1c2aac4b9a4df9a20daa1446b16b3309 Mon Sep 17 00:00:00 2001 From: Evyweb Date: Sat, 9 Nov 2024 16:18:38 +0100 Subject: [PATCH] feat: add scopes support --- README.md | 64 +++++++- specs/container.spec.ts | 320 +++++++++++++++++++++++++++++++++++----- src/container.ts | 73 ++++++--- src/module.ts | 53 ++++--- src/types.ts | 47 ++++-- 5 files changed, 469 insertions(+), 88 deletions(-) diff --git a/README.md b/README.md index f218f55..dd91e5b 100644 --- a/README.md +++ b/README.md @@ -143,9 +143,13 @@ myUseCase.execute(); Code used in the examples can be found in the specs folder. -### Loading modules +### Modules -You can also use modules to organize your dependencies. Modules can then be loaded in your container. +You can also use modules to organize your dependencies. + +#### Loading modules + +Modules can then be loaded in your container. By default, when you create a container, it is using a default module under the hood. ```typescript @@ -168,7 +172,7 @@ const myService = container.get(DI.MY_SERVICE); The dependencies do not need to be registered in the same module as the one that is using them. Note that the module name used as a key is a symbol. -### Modules override +#### Modules override You can also override dependencies of a module. The dependencies of the module will be overridden by the dependencies of the last loaded module. @@ -193,7 +197,7 @@ container.load(Symbol('module3'), module3); const myService = container.get(DI.MY_SERVICE); ``` -### Unload modules +#### Unload modules You can also unload a module from the container. The dependencies of the module will be removed from the container. Already cached instances will be removed to keep consistency and avoid potential errors. @@ -210,3 +214,55 @@ container.unload(Symbol('module1')); // Will throw an error as the dependency is not registered anymore const myService = container.get(DI.DEP1); ``` +### Using scopes + +#### Singleton scope (default) + +In singleton scope, the container returns the same instance every time a dependency is resolved. + +```typescript +container.bind(DI.MY_SERVICE).toClass(MyServiceClass, [DI.DEP1, DI.DEP2]); +// or +container.bind(DI.MY_SERVICE).toClass(MyServiceClass, [DI.DEP1, DI.DEP2], 'singleton'); + +const instance1 = container.get(DI.MY_SERVICE); +const instance2 = container.get(DI.MY_SERVICE); + +console.log(instance1 === instance2); // true +``` +#### Transient scope + +In transient scope, the container returns a new instance every time the dependency is resolved. + +```typescript +container.bind(DI.MY_SERVICE).toClass(MyServiceClass, [DI.DEP1, DI.DEP2], 'transient'); + +const instance1 = container.get(DI.MY_SERVICE); +const instance2 = container.get(DI.MY_SERVICE); + +console.log(instance1 === instance2); // false +``` + +#### Scoped Scope +In scoped scope, the container returns the same instance within a scope. Different scopes will have different instances. + +To use the scoped scope, you need to create a scope using runInScope. + +```typescript +container.bind(DI.MY_SERVICE).toClass(MyServiceClass, [DI.DEP1, DI.DEP2], 'scoped'); + +container.runInScope(() => { + const instance1 = container.get(DI.MY_SERVICE); + const instance2 = container.get(DI.MY_SERVICE); + + console.log(instance1 === instance2); // true +}); + +container.runInScope(() => { + const instance3 = container.get(DI.MY_SERVICE); + + console.log(instance3 === instance1); // false +}); +``` + +Note: If you try to resolve a scoped dependency outside a scope, an error will be thrown. diff --git a/specs/container.spec.ts b/specs/container.spec.ts index 7809c2f..8c1fe9b 100644 --- a/specs/container.spec.ts +++ b/specs/container.spec.ts @@ -14,6 +14,7 @@ import {HigherOrderFunctionWithoutDependencies} from "./HigherOrderFunctionWitho import {ServiceWithoutDependencyInterface} from "./ServiceWithoutDependencyInterface"; import {MyServiceClassWithoutDependencies} from "./MyServiceClassWithoutDependencies"; import {mock, MockProxy} from "vitest-mock-extended"; +import {vi} from "vitest"; describe('Container', () => { @@ -77,6 +78,77 @@ describe('Container', () => { .toThrowError('Invalid dependencies type'); }); }); + + describe('When the scope is defined to "transient"', () => { + it('should return a new instance each time', () => { + // Arrange + container.bind(DI.DEP1).toValue('dependency1'); + container.bind(DI.DEP2).toValue(42); + container.bind(DI.MY_SERVICE) + .toHigherOrderFunction(MyService, {dep1: DI.DEP1, dep2: DI.DEP2}, 'transient'); + + // Act + const myService1 = container.get(DI.MY_SERVICE); + const myService2 = container.get(DI.MY_SERVICE); + + // Assert + expect(myService1).not.toBe(myService2); + }); + }); + + describe('When the scope is defined to "scoped"', () => { + it('should return the same instance within the same scope', () => { + // Arrange + container.bind(DI.DEP1).toValue('dependency1'); + container.bind(DI.DEP2).toValue(42); + container.bind(DI.MY_SERVICE) + .toHigherOrderFunction(MyService, {dep1: DI.DEP1, dep2: DI.DEP2}, 'scoped'); + + // Act & Assert + container.runInScope(() => { + const myService1 = container.get(DI.MY_SERVICE); + const myService2 = container.get(DI.MY_SERVICE); + expect(myService1).toBe(myService2); + }); + }); + + it('should return different instances in different scopes', () => { + // Arrange + container.bind(DI.DEP1).toValue('dependency1'); + container.bind(DI.DEP2).toValue(42); + container.bind(DI.MY_SERVICE) + .toHigherOrderFunction(MyService, {dep1: DI.DEP1, dep2: DI.DEP2}, 'scoped'); + + // Act + let myService1: MyServiceInterface | undefined; + let myService2: MyServiceInterface | undefined; + + container.runInScope(() => { + myService1 = container.get(DI.MY_SERVICE); + }); + + container.runInScope(() => { + myService2 = container.get(DI.MY_SERVICE); + }); + + // Assert + expect(myService1).toBeDefined(); + expect(myService2).toBeDefined(); + expect(myService1).not.toBe(myService2); + }); + }); + + describe('When a scoped dependency is resolved outside of a scope', () => { + it('should throw an error', () => { + // Arrange + container.bind(DI.MY_SERVICE) + .toHigherOrderFunction(MyService, {dep1: DI.DEP1, dep2: DI.DEP2}, 'scoped'); + + // Act & Assert + expect(() => container.get(DI.MY_SERVICE)) + .toThrowError(`Cannot resolve scoped binding outside of a scope: ${DI.MY_SERVICE.toString()}`); + }); + }); }); describe.each([ @@ -155,28 +227,114 @@ describe('Container', () => { }); }); - describe('When the dependency is retrieved twice', () => { - it('should return the same instance', () => { - // Arrange - const factoryCalls = vi.fn(); - container.bind(DI.DEP1).toValue('dependency1'); - container.bind(DI.DEP2).toValue(42); + describe('When the instance is retrieved twice', () => { + describe('When the scope is not defined', () => { + it('should return the same instance (singleton)', () => { + // Arrange + const factoryCalls = vi.fn(); + container.bind(DI.DEP1).toValue('dependency1'); + container.bind(DI.DEP2).toValue(42); + + container.bind(DI.MY_SERVICE).toFactory(() => { + factoryCalls(); + return MyService({ + dep1: container.get(DI.DEP1), + dep2: container.get(DI.DEP2) + }); + }); + const myService1 = container.get(DI.MY_SERVICE); - container.bind(DI.MY_SERVICE).toFactory(() => { - factoryCalls(); - return MyService({ - dep1: container.get(DI.DEP1), - dep2: container.get(DI.DEP2) + // Act + const myService2 = container.get(DI.MY_SERVICE); + + // Assert + expect(myService1).toBe(myService2); + expect(factoryCalls).toHaveBeenCalledTimes(1); + }); + }); + + describe('When the scope is defined to "transient"', () => { + it('should return a new instance each time', () => { + // Arrange + const factoryCalls = vi.fn(); + container.bind(DI.DEP1).toValue('dependency1'); + container.bind(DI.DEP2).toValue(42); + + container.bind(DI.MY_SERVICE).toFactory(() => { + factoryCalls(); + return MyService({ + dep1: container.get(DI.DEP1), + dep2: container.get(DI.DEP2) + }); + }, 'transient'); + + // Act + const myService1 = container.get(DI.MY_SERVICE); + const myService2 = container.get(DI.MY_SERVICE); + + // Assert + expect(myService1).not.toBe(myService2); + expect(factoryCalls).toHaveBeenCalledTimes(2); + }); + }); + + describe('When the scope is defined to "scoped"', () => { + it('should return the same instance within the same scope', () => { + // Arrange + const factoryCalls = vi.fn(); + container.bind(DI.DEP1).toValue('dependency1'); + container.bind(DI.DEP2).toValue(42); + + container.bind(DI.MY_SERVICE).toFactory(() => { + factoryCalls(); + return MyService({ + dep1: container.get(DI.DEP1), + dep2: container.get(DI.DEP2) + }); + }, 'scoped'); + + // Act & Assert + container.runInScope(() => { + const myService1 = container.get(DI.MY_SERVICE); + const myService2 = container.get(DI.MY_SERVICE); + + expect(myService1).toBe(myService2); + expect(factoryCalls).toHaveBeenCalledTimes(1); }); }); - const myService1 = container.get(DI.MY_SERVICE); - // Act - const myService2 = container.get(DI.MY_SERVICE); + it('should return different instances in different scopes', () => { + // Arrange + const factoryCalls = vi.fn(); + container.bind(DI.DEP1).toValue('dependency1'); + container.bind(DI.DEP2).toValue(42); + + container.bind(DI.MY_SERVICE).toFactory(() => { + factoryCalls(); + return MyService({ + dep1: container.get(DI.DEP1), + dep2: container.get(DI.DEP2) + }); + }, 'scoped'); + + // Act + let myService1: MyServiceInterface | undefined; + let myService2: MyServiceInterface | undefined; + + container.runInScope(() => { + myService1 = container.get(DI.MY_SERVICE); + }); - // Assert - expect(myService1).toBe(myService2); - expect(factoryCalls).toHaveBeenCalledTimes(1); + container.runInScope(() => { + myService2 = container.get(DI.MY_SERVICE); + }); + + // Assert + expect(myService1).toBeDefined(); + expect(myService2).toBeDefined(); + expect(myService1).not.toBe(myService2); + expect(factoryCalls).toHaveBeenCalledTimes(2); + }); }); }); }); @@ -201,8 +359,6 @@ describe('Container', () => { describe('When the class has no dependency', () => { it('should just return the instance', () => { // Arrange - container.bind(DI.DEP1).toValue('dependency1'); - container.bind(DI.DEP2).toValue(42); container.bind(DI.CLASS_WITHOUT_DEPENDENCIES).toClass(MyServiceClassWithoutDependencies); // Act @@ -214,18 +370,105 @@ describe('Container', () => { }); describe('When the instance is retrieved twice', () => { - it('should always return the same instance', () => { - // Arrange - container.bind(DI.DEP1).toValue('dependency1'); - container.bind(DI.DEP2).toValue(42); - container.bind(DI.CLASS_WITH_DEPENDENCIES).toClass(MyServiceClass, [DI.DEP1, DI.DEP2]); - const myService1 = container.get(DI.CLASS_WITH_DEPENDENCIES); + describe('When the scope is not defined', () => { + it('should return the same instance (singleton)', () => { + // Arrange + container.bind(DI.DEP1).toValue('dependency1'); + container.bind(DI.DEP2).toValue(42); + container.bind(DI.CLASS_WITH_DEPENDENCIES).toClass(MyServiceClass, [DI.DEP1, DI.DEP2]); + const myService1 = container.get(DI.CLASS_WITH_DEPENDENCIES); - // Act - const myService2 = container.get(DI.CLASS_WITH_DEPENDENCIES); + // Act + const myService2 = container.get(DI.CLASS_WITH_DEPENDENCIES); - // Assert - expect(myService1).toBe(myService2); + // Assert + expect(myService1).toBe(myService2); + }); + }); + + describe('When the scope is defined to "singleton"', () => { + it('should return the same instance', () => { + // Arrange + container.bind(DI.DEP1).toValue('dependency1'); + container.bind(DI.DEP2).toValue(42); + container.bind(DI.CLASS_WITH_DEPENDENCIES).toClass(MyServiceClass, [DI.DEP1, DI.DEP2], 'singleton'); + const myService1 = container.get(DI.CLASS_WITH_DEPENDENCIES); + + // Act + const myService2 = container.get(DI.CLASS_WITH_DEPENDENCIES); + + // Assert + expect(myService1).toBe(myService2); + }); + }); + + describe('When the scope is defined to "transient"', () => { + it('should return a new instance each time', () => { + // Arrange + container.bind(DI.DEP1).toValue('dependency1'); + container.bind(DI.DEP2).toValue(42); + container.bind(DI.CLASS_WITH_DEPENDENCIES).toClass(MyServiceClass, [DI.DEP1, DI.DEP2], 'transient'); + + // Act + const myService1 = container.get(DI.CLASS_WITH_DEPENDENCIES); + const myService2 = container.get(DI.CLASS_WITH_DEPENDENCIES); + + // Assert + expect(myService1).not.toBe(myService2); + }); + }); + + describe('When the scope is defined to "scoped"', () => { + it('should return the same instance within the same scope', () => { + // Arrange + container.bind(DI.DEP1).toValue('dependency1'); + container.bind(DI.DEP2).toValue(42); + container.bind(DI.CLASS_WITH_DEPENDENCIES).toClass(MyServiceClass, [DI.DEP1, DI.DEP2], 'scoped'); + + // Act + container.runInScope(() => { + const myService1 = container.get(DI.CLASS_WITH_DEPENDENCIES); + const myService2 = container.get(DI.CLASS_WITH_DEPENDENCIES); + + // Assert + expect(myService1).toBe(myService2); + }); + }); + + it('should return different instances in different scopes', () => { + // Arrange + container.bind(DI.DEP1).toValue('dependency1'); + container.bind(DI.DEP2).toValue(42); + container.bind(DI.CLASS_WITH_DEPENDENCIES).toClass(MyServiceClass, [DI.DEP1, DI.DEP2], 'scoped'); + + // Act + let myService1: MyServiceClassInterface | undefined; + let myService2: MyServiceClassInterface | undefined; + + container.runInScope(() => { + myService1 = container.get(DI.CLASS_WITH_DEPENDENCIES); + }); + + container.runInScope(() => { + myService2 = container.get(DI.CLASS_WITH_DEPENDENCIES); + }); + + // Assert + expect(myService1).toBeDefined(); + expect(myService2).toBeDefined(); + expect(myService1).not.toBe(myService2); + }); + }); + + describe('When a scoped dependency is resolved outside of a scope', () => { + it('should throw an error', () => { + // Arrange + container.bind(DI.CLASS_WITH_DEPENDENCIES).toClass(MyServiceClass, [DI.DEP1, DI.DEP2], 'scoped'); + + // Act & Assert + expect(() => container.get(DI.CLASS_WITH_DEPENDENCIES)) + .toThrowError(`Cannot resolve scoped binding outside of a scope: ${DI.CLASS_WITH_DEPENDENCIES.toString()}`); + }); }); }); }); @@ -252,7 +495,7 @@ describe('Container', () => { expect(sayHello()).toBe('hello world'); }); - describe('When a dependency of the module is registered in an other module', () => { + describe('When a dependency of the module is registered in another module', () => { it('should correctly resolve all dependencies', () => { // Arrange const module1 = createModule(); @@ -264,7 +507,6 @@ describe('Container', () => { const module3 = createModule(); module3.bind(DI.MY_SERVICE).toHigherOrderFunction(MyService, {dep1: DI.DEP1, dep2: DI.DEP2}); - const container = createContainer(); container.load(Symbol('module1'), module1); container.load(Symbol('module2'), module2); container.load(Symbol('module3'), module3); @@ -288,7 +530,6 @@ describe('Container', () => { const module3 = createModule(); module3.bind(DI.MY_SERVICE).toHigherOrderFunction(MyService, {dep1: DI.DEP1, dep2: DI.DEP2}); - const container = createContainer(); container.bind(DI.DEP2).toValue(42); container.load(Symbol('module1'), module1); container.load(Symbol('module2'), module2); @@ -304,7 +545,7 @@ describe('Container', () => { }); describe('When a module is unloaded', () => { - describe('When an other module has this dependency already registered', () => { + describe('When another module has this dependency already registered', () => { it('should use the existing dependency', () => { // Arrange const MODULE1 = Symbol('myModule1'); @@ -353,4 +594,15 @@ describe('Container', () => { }); }); }); -}); \ No newline at end of file + + describe('When an unknown scope is used during binding', () => { + it('should throw an error', () => { + // Arrange + container.bind(DI.MY_SERVICE).toFunction(sayHelloWorld, 'unknown' as any); + + // Act & Assert + expect(() => container.get(DI.MY_SERVICE)) + .toThrowError('Unknown scope: unknown'); + }); + }); +}); diff --git a/src/container.ts b/src/container.ts index 86b4718..03d5e30 100644 --- a/src/container.ts +++ b/src/container.ts @@ -1,53 +1,84 @@ -import {Container, Module} from "./types"; +import {Binding, Container, Module} from "./types"; import {createModule} from "./module"; -const DEFAULT_MODULE = Symbol('DEFAULT'); - export function createContainer(): Container { const modules = new Map(); - const instances = new Map(); + const singletonInstances = new Map(); + const scopedInstances = new Map>(); + let currentScopeId: symbol | undefined; const defaultModule = createModule(); - modules.set(DEFAULT_MODULE, defaultModule); + modules.set(Symbol('DEFAULT'), defaultModule); const bind = (key: symbol) => defaultModule.bind(key); const load = (moduleKey: symbol, module: Module) => modules.set(moduleKey, module); const unload = (moduleKey: symbol) => { - instances.clear(); + singletonInstances.clear(); modules.delete(moduleKey); }; - const findLastBinding = (key: symbol): CallableFunction | null => { + const findLastBinding = (key: symbol): Binding | null => { const modulesArray = Array.from(modules.values()); - const moduleLength = modulesArray.length - 1; - - for (let i = moduleLength; i >= 0; i--) { + for (let i = modulesArray.length - 1; i >= 0; i--) { const module = modulesArray[i]; const binding = module.bindings.get(key); if (binding) { - return binding; + return binding as Binding; } } - return null; }; const get = (key: symbol): T => { - if (instances.has(key)) { - return instances.get(key) as T; + const binding = findLastBinding(key); + if (!binding) throw new Error(`No binding found for key: ${key.toString()}`); + + const { factory, scope } = binding; + + if (scope === 'singleton') { + if (!singletonInstances.has(key)) { + singletonInstances.set(key, factory(resolveDependency)); + } + return singletonInstances.get(key) as T; } - const binding = findLastBinding(key); - if (!binding) { - throw new Error(`No binding found for key: ${key.toString()}`); + if (scope === 'transient') { + return factory(resolveDependency) as T; } - const instance = binding((depKey: symbol) => get(depKey)); - instances.set(key, instance); - return instance as T; + if (scope === 'scoped') { + if (!currentScopeId) throw new Error(`Cannot resolve scoped binding outside of a scope: ${key.toString()}`); + + if (!scopedInstances.has(currentScopeId)) { + scopedInstances.set(currentScopeId, new Map()); + } + const scopeMap = scopedInstances.get(currentScopeId)!; + if (!scopeMap.has(key)) { + scopeMap.set(key, factory(resolveDependency)); + } + + return scopeMap.get(key) as T; + } + + throw new Error(`Unknown scope: ${scope}`); + }; + + const resolveDependency = (depKey: symbol): unknown => { + return get(depKey); + }; + + const runInScope = (callback: () => T): T => { + const previousScopeId = currentScopeId; + currentScopeId = Symbol('scope'); + try { + return callback(); + } finally { + scopedInstances.delete(currentScopeId); + currentScopeId = previousScopeId; + } }; - return {bind, load, get, unload}; + return { bind, load, get, unload, runInScope }; } diff --git a/src/module.ts b/src/module.ts index 5d68482..56cc72f 100644 --- a/src/module.ts +++ b/src/module.ts @@ -1,7 +1,12 @@ -import {DependencyArray, DependencyObject, Module, ResolveFunction} from "./types"; +import { DependencyArray, DependencyObject, Module, ResolveFunction } from "./types"; + +interface Binding { + factory: (resolve: ResolveFunction) => unknown; + scope: 'singleton' | 'transient' | 'scoped'; +} export function createModule(): Module { - const bindings = new Map(); + const bindings = new Map(); const resolveDependenciesArray = (dependencies: DependencyArray, resolve: ResolveFunction) => dependencies.map(resolve); @@ -10,25 +15,31 @@ export function createModule(): Module { return Object.fromEntries(entries.map(([key, dependency]) => [key, resolve(dependency)])); }; - const isDependencyArray = (dependencies: DependencyArray | DependencyObject): dependencies is DependencyArray => Array.isArray(dependencies); + const isDependencyArray = (dependencies: DependencyArray | DependencyObject): dependencies is DependencyArray => + Array.isArray(dependencies); - const isDependencyObject = (dependencies: DependencyArray | DependencyObject): dependencies is DependencyObject => dependencies !== null && typeof dependencies === 'object' && !Array.isArray(dependencies); + const isDependencyObject = (dependencies: DependencyArray | DependencyObject): dependencies is DependencyObject => + dependencies !== null && typeof dependencies === 'object' && !Array.isArray(dependencies); const bind = (key: symbol) => { - const toValue = (value: unknown) => { - bindings.set(key, () => value); + const toValue = (value: unknown, scope: 'singleton' | 'transient' | 'scoped' = 'singleton') => { + bindings.set(key, { factory: () => value, scope }); }; - const toFunction = (fn: CallableFunction) => { - bindings.set(key, () => fn); + const toFunction = (fn: CallableFunction, scope: 'singleton' | 'transient' | 'scoped' = 'singleton') => { + bindings.set(key, { factory: () => fn, scope }); }; - const toHigherOrderFunction = (fn: CallableFunction, dependencies?: DependencyArray | DependencyObject) => { + const toHigherOrderFunction = ( + fn: CallableFunction, + dependencies?: DependencyArray | DependencyObject, + scope: 'singleton' | 'transient' | 'scoped' = 'singleton' + ) => { if (dependencies && !isDependencyArray(dependencies) && !isDependencyObject(dependencies)) { throw new Error('Invalid dependencies type'); } - bindings.set(key, (resolve: ResolveFunction) => { + const factory = (resolve: ResolveFunction) => { if (!dependencies) { return fn(); } @@ -38,18 +49,26 @@ export function createModule(): Module { } return fn({ ...resolveDependenciesObject(dependencies, resolve) }); - }); + }; + + bindings.set(key, { factory, scope }); }; - const toFactory = (factory: CallableFunction) => { - bindings.set(key, (resolve: ResolveFunction) => factory(resolve)); + const toFactory = (factory: CallableFunction, scope: 'singleton' | 'transient' | 'scoped' = 'singleton') => { + bindings.set(key, { factory: (resolve: ResolveFunction) => factory(resolve), scope }); }; - const toClass = (AnyClass: new (...args: unknown[]) => unknown, dependencies: DependencyArray = []) => { - bindings.set(key, (resolve: ResolveFunction) => { + const toClass = ( + AnyClass: new (...args: unknown[]) => unknown, + dependencies: DependencyArray = [], + scope: 'singleton' | 'transient' | 'scoped' = 'singleton' + ) => { + const factory = (resolve: ResolveFunction) => { const resolvedDeps = dependencies.map(dep => resolve(dep)); return new AnyClass(...resolvedDeps); - }); + }; + + bindings.set(key, { factory, scope }); }; return { @@ -61,5 +80,5 @@ export function createModule(): Module { }; }; - return {bind, bindings}; + return { bind, bindings }; } diff --git a/src/types.ts b/src/types.ts index dc87c49..c773b05 100644 --- a/src/types.ts +++ b/src/types.ts @@ -6,11 +6,19 @@ export type DependencyArray = symbol[]; export interface Container { bind(key: symbol): { - toValue: (value: unknown) => void; - toFunction: (fn: CallableFunction) => void; - toHigherOrderFunction: (fn: CallableFunction, dependencies?: DependencyArray | DependencyObject) => void; - toFactory: (factory: CallableFunction) => void; - toClass: (constructor: new (...args: any[]) => C, dependencies?: DependencyArray) => void; + toValue: (value: unknown, scope?: 'singleton' | 'transient' | 'scoped') => void; + toFunction: (fn: CallableFunction, scope?: 'singleton' | 'transient' | 'scoped') => void; + toHigherOrderFunction: ( + fn: CallableFunction, + dependencies?: DependencyArray | DependencyObject, + scope?: 'singleton' | 'transient' | 'scoped' + ) => void; + toFactory: (factory: CallableFunction, scope?: 'singleton' | 'transient' | 'scoped') => void; + toClass: ( + constructor: new (...args: any[]) => C, + dependencies?: DependencyArray, + scope?: 'singleton' | 'transient' | 'scoped' + ) => void; }; load(moduleKey: symbol, module: Module): void; @@ -18,22 +26,37 @@ export interface Container { get(key: symbol): T; unload(key: symbol): void; + + runInScope(callback: () => T): T; } export interface Module { bind(key: symbol): { - toValue: (value: unknown) => void; - toFunction: (fn: CallableFunction) => void; - toHigherOrderFunction: (fn: CallableFunction, dependencies?: DependencyArray | DependencyObject) => void; - toFactory: (factory: CallableFunction) => void; - toClass: (constructor: new (...args: any[]) => C, dependencies?: DependencyArray) => void; + toValue: (value: unknown, scope?: 'singleton' | 'transient' | 'scoped') => void; + toFunction: (fn: CallableFunction, scope?: 'singleton' | 'transient' | 'scoped') => void; + toHigherOrderFunction: ( + fn: CallableFunction, + dependencies?: DependencyArray | DependencyObject, + scope?: 'singleton' | 'transient' | 'scoped' + ) => void; + toFactory: (factory: CallableFunction, scope?: 'singleton' | 'transient' | 'scoped') => void; + toClass: ( + constructor: new (...args: any[]) => C, + dependencies?: DependencyArray, + scope?: 'singleton' | 'transient' | 'scoped' + ) => void; }; - bindings: Map; + bindings: Map; } export interface InjectionTokens { [key: string]: symbol; } -export type ResolveFunction = (dep: symbol) => unknown; \ No newline at end of file +export type ResolveFunction = (dep: symbol) => unknown; + +export interface Binding { + factory: (resolve: (key: symbol) => unknown) => unknown; + scope: 'singleton' | 'transient' | 'scoped'; +} \ No newline at end of file