Skip to content

Commit

Permalink
feat: modules and scopes support
Browse files Browse the repository at this point in the history
  • Loading branch information
Evyweb authored Nov 10, 2024
1 parent a5f9828 commit 4957dfb
Show file tree
Hide file tree
Showing 24 changed files with 706 additions and 121 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# @evyweb/ioctopus

## 0.3.0

### Minor Changes

- Modules support
- Scopes support

## 0.2.2

### Patch Changes
Expand Down
124 changes: 124 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,127 @@ myUseCase.execute();
```

Code used in the examples can be found in the specs folder.

### Modules

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
const module1 = createModule();
module1.bind(DI.DEP1).toValue('dependency1');

const module2 = createModule();
module2.bind(DI.DEP2).toValue(42);

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);

const myService = container.get<MyServiceInterface>(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

You can also override dependencies of a module. The dependencies of the module will be overridden by the dependencies of the last loaded module.

```typescript
const module1 = createModule();
module1.bind(DI.DEP1).toValue('OLD dependency1');
module1.bind(DI.MY_SERVICE).toFunction(sayHelloWorld);

const module2 = createModule();
module2.bind(DI.DEP1).toValue('NEW dependency1');

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); // Default module
container.load(Symbol('module1'), module1);
container.load(Symbol('module2'), module2);
container.load(Symbol('module3'), module3);

// The dependency DI.MY_SERVICE will be resolved with the higher order function and dep1 will be 'NEW dependency1'
const myService = container.get<MyServiceInterface>(DI.MY_SERVICE);
```

#### 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.

```typescript
const module1 = createModule();
module1.bind(DI.DEP1).toValue('dependency1');

const container = createContainer();
container.load(Symbol('module1'), module1);

container.unload(Symbol('module1'));

// Will throw an error as the dependency is not registered anymore
const myService = container.get<string>(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<MyServiceClassInterface>(DI.MY_SERVICE);
const instance2 = container.get<MyServiceClassInterface>(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<MyServiceClassInterface>(DI.MY_SERVICE);
const instance2 = container.get<MyServiceClassInterface>(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<MyServiceClassInterface>(DI.MY_SERVICE);
const instance2 = container.get<MyServiceClassInterface>(DI.MY_SERVICE);

console.log(instance1 === instance2); // true
});

container.runInScope(() => {
const instance3 = container.get<MyServiceClassInterface>(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.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@evyweb/ioctopus",
"version": "0.2.2",
"version": "0.3.0",
"description": "A simple IoC container for JavaScript and TypeScript for classes and functions.",
"main": "dist/index.js",
"module": "dist/index.mjs",
Expand Down
79 changes: 18 additions & 61 deletions specs/container.spec.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import {MyService} from "./MyService";
import {MyServiceInterface} from "./MyServiceInterface";
import {SayHelloType} from "./SayHelloType";
import {sayHelloWorld} from "./sayHelloWorld";
import {MyUseCase} from "./MyUseCase";
import {MyUseCaseInterface} from "./MyUseCaseInterface";
import {LoggerInterface} from "./LoggerInterface";
import {DI} from "./DI";
import {MyService} from "./examples/MyService";
import {MyServiceInterface} from "./examples/MyServiceInterface";
import {SayHelloType} from "./examples/SayHelloType";
import {sayHelloWorld} from "./examples/sayHelloWorld";
import {MyUseCase} from "./examples/MyUseCase";
import {MyUseCaseInterface} from "./examples/MyUseCaseInterface";
import {LoggerInterface} from "./examples/LoggerInterface";
import {DI} from "./examples/DI";
import {Container, createContainer} from "../src";
import {MyServiceClass} from "./MyServiceClass";
import {MyServiceClassInterface} from "./MyServiceClassInterface";
import {FunctionWithDependencies} from "./FunctionWithDependencies";
import {HigherOrderFunctionWithoutDependencies} from "./HigherOrderFunctionWithoutDependencies";
import {ServiceWithoutDependencyInterface} from "./ServiceWithoutDependencyInterface";
import {MyServiceClassWithoutDependencies} from "./MyServiceClassWithoutDependencies";
import {MyServiceClass} from "./examples/MyServiceClass";
import {MyServiceClassInterface} from "./examples/MyServiceClassInterface";
import {FunctionWithDependencies} from "./examples/FunctionWithDependencies";
import {HigherOrderFunctionWithoutDependencies} from "./examples/HigherOrderFunctionWithoutDependencies";
import {ServiceWithoutDependencyInterface} from "./examples/ServiceWithoutDependencyInterface";
import {MyServiceClassWithoutDependencies} from "./examples/MyServiceClassWithoutDependencies";
import {mock, MockProxy} from "vitest-mock-extended";

describe('Container', () => {
Expand Down Expand Up @@ -80,9 +80,9 @@ describe('Container', () => {
});

describe.each([
{ dependencies: undefined },
{ dependencies: [] },
{ dependencies: {} },
{dependencies: undefined},
{dependencies: []},
{dependencies: {}},
])('When the function is a higher order function without dependencies', ({dependencies}) => {
it('should just return the function', () => {
// Arrange
Expand Down Expand Up @@ -154,31 +154,6 @@ describe('Container', () => {
expect(fakeLogger.log).toHaveBeenCalledWith('hello world');
});
});

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);

container.bind(DI.MY_SERVICE).toFactory(() => {
factoryCalls();
return MyService({
dep1: container.get<string>(DI.DEP1),
dep2: container.get<number>(DI.DEP2)
});
});
const myService1 = container.get<MyServiceInterface>(DI.MY_SERVICE);

// Act
const myService2 = container.get<MyServiceInterface>(DI.MY_SERVICE);

// Assert
expect(myService1).toBe(myService2);
expect(factoryCalls).toHaveBeenCalledTimes(1);
});
});
});
});

Expand All @@ -201,8 +176,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
Expand All @@ -212,22 +185,6 @@ describe('Container', () => {
expect(myService.runTask()).toBe('Executing without dependencies');
});
});

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<MyServiceClassInterface>(DI.CLASS_WITH_DEPENDENCIES);

// Act
const myService2 = container.get<MyServiceClassInterface>(DI.CLASS_WITH_DEPENDENCIES);

// Assert
expect(myService1).toBe(myService2);
});
});
});

describe('When no dependency has been registered', () => {
Expand All @@ -237,4 +194,4 @@ describe('Container', () => {
.toThrowError(`No binding found for key: ${DI.NOT_REGISTERED_VALUE.toString()}`);
});
});
});
});
2 changes: 1 addition & 1 deletion specs/DI.ts → specs/examples/DI.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {InjectionTokens} from "../src";
import {InjectionTokens} from "../../src";

export const DI: InjectionTokens = {
DEP1: Symbol('DEP1'),
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loading

0 comments on commit 4957dfb

Please sign in to comment.