feat: implement service collection
This commit is contained in:
@@ -3,3 +3,4 @@ import "reflect-metadata";
|
||||
export * from './serviceProvider.js';
|
||||
export * from './types.js';
|
||||
export * from './decorators.js';
|
||||
export * from './serviceCollection.js';
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { ServiceProvider } from './serviceProvider.js';
|
||||
import type {
|
||||
Constructor,
|
||||
IServiceCollection,
|
||||
IServiceProvider,
|
||||
ServiceDescriptor,
|
||||
ServiceFactory,
|
||||
ServiceIdentifier,
|
||||
ServiceLifetime,
|
||||
} from './types.js';
|
||||
|
||||
export class ServiceCollection implements IServiceCollection {
|
||||
private readonly _descriptors: Map<ServiceIdentifier<unknown>, ServiceDescriptor<unknown>> =
|
||||
new Map();
|
||||
|
||||
public addSingleton<T>(
|
||||
serviceType: ServiceIdentifier<T>,
|
||||
implementationOrFactory: ServiceFactory<T> | Constructor<T>,
|
||||
): IServiceCollection {
|
||||
return this.add(serviceType, implementationOrFactory, 'singleton');
|
||||
}
|
||||
|
||||
public addScoped<T>(
|
||||
serviceType: ServiceIdentifier<T>,
|
||||
implementationOrFactory: ServiceFactory<T> | Constructor<T>,
|
||||
): IServiceCollection {
|
||||
return this.add(serviceType, implementationOrFactory, 'scoped');
|
||||
}
|
||||
|
||||
public addTransient<T>(
|
||||
serviceType: ServiceIdentifier<T>,
|
||||
implementationOrFactory: ServiceFactory<T> | Constructor<T>,
|
||||
): IServiceCollection {
|
||||
return this.add(serviceType, implementationOrFactory, 'transient');
|
||||
}
|
||||
|
||||
public build(): IServiceProvider {
|
||||
return new ServiceProvider(this._descriptors);
|
||||
}
|
||||
|
||||
private add<T>(
|
||||
serviceType: ServiceIdentifier<T>,
|
||||
implementationOrFactory: ServiceFactory<T> | Constructor<T>,
|
||||
lifetime: ServiceLifetime,
|
||||
): IServiceCollection {
|
||||
if (typeof implementationOrFactory === 'function' && this.isConstructor(implementationOrFactory) === false) {
|
||||
const descriptor: ServiceDescriptor<T> = {
|
||||
serviceType,
|
||||
implementationType: Object as unknown as Constructor<T>,
|
||||
lifetime,
|
||||
factory: implementationOrFactory as ServiceFactory<T>,
|
||||
};
|
||||
|
||||
this._descriptors.set(serviceType, descriptor);
|
||||
} else {
|
||||
const descriptor: ServiceDescriptor<T> = {
|
||||
serviceType,
|
||||
implementationType: implementationOrFactory as Constructor<T>,
|
||||
lifetime,
|
||||
};
|
||||
|
||||
this._descriptors.set(serviceType, descriptor);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
private isConstructor(func: unknown): func is Constructor<unknown> {
|
||||
return typeof func === 'function' &&
|
||||
!!func.prototype &&
|
||||
func.prototype.constructor === func;
|
||||
}
|
||||
}
|
||||
+15
-15
@@ -19,19 +19,19 @@ export class ServiceScope implements IServiceScope {
|
||||
}
|
||||
|
||||
export class ServiceProvider implements IServiceProvider {
|
||||
private descriptors: Map<ServiceIdentifier<unknown>, ServiceDescriptor<unknown>>;
|
||||
private singletonInstances: Map<ServiceIdentifier<unknown>, unknown> = new Map();
|
||||
private scopedInstances: Map<ServiceIdentifier<unknown>, unknown> = new Map();
|
||||
private readonly _descriptors: Map<ServiceIdentifier<unknown>, ServiceDescriptor<unknown>>;
|
||||
private readonly _singletonInstances: Map<ServiceIdentifier<unknown>, unknown> = new Map();
|
||||
private readonly _scopedInstances: Map<ServiceIdentifier<unknown>, unknown> = new Map();
|
||||
|
||||
constructor(
|
||||
descriptors: Map<ServiceIdentifier<unknown>, ServiceDescriptor<unknown>>,
|
||||
parent?: ServiceProvider,
|
||||
) {
|
||||
this.descriptors = descriptors;
|
||||
this._descriptors = descriptors;
|
||||
|
||||
if (parent) {
|
||||
parent.singletonInstances.forEach((value, key) => {
|
||||
this.singletonInstances.set(key, value);
|
||||
parent._singletonInstances.forEach((value, key) => {
|
||||
this._singletonInstances.set(key, value);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ export class ServiceProvider implements IServiceProvider {
|
||||
}
|
||||
|
||||
getService<T>(serviceType: ServiceIdentifier<T>): T {
|
||||
const descriptor = this.descriptors.get(serviceType);
|
||||
const descriptor = this._descriptors.get(serviceType);
|
||||
|
||||
if (!descriptor) {
|
||||
throw new Error(`Service of type ${serviceType.toString()} is not registered.`);
|
||||
@@ -53,12 +53,12 @@ export class ServiceProvider implements IServiceProvider {
|
||||
}
|
||||
|
||||
createScope(): IServiceScope {
|
||||
const scopedProvider = new ServiceProvider(this.descriptors, this);
|
||||
const scopedProvider = new ServiceProvider(this._descriptors, this);
|
||||
return new ServiceScope(scopedProvider);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.scopedInstances.clear();
|
||||
this._scopedInstances.clear();
|
||||
}
|
||||
|
||||
private resolveService<T>(descriptor: ServiceDescriptor<T>): T {
|
||||
@@ -66,19 +66,19 @@ export class ServiceProvider implements IServiceProvider {
|
||||
|
||||
switch (lifetime) {
|
||||
case 'singleton': {
|
||||
if (this.singletonInstances.has(serviceType)) {
|
||||
return this.singletonInstances.get(serviceType) as T;
|
||||
if (this._singletonInstances.has(serviceType)) {
|
||||
return this._singletonInstances.get(serviceType) as T;
|
||||
}
|
||||
const singletonInstance = factory ? factory(this) : this.createInstance(implementationType);
|
||||
this.singletonInstances.set(serviceType, singletonInstance);
|
||||
this._singletonInstances.set(serviceType, singletonInstance);
|
||||
return singletonInstance;
|
||||
}
|
||||
case 'scoped': {
|
||||
if (this.scopedInstances.has(serviceType)) {
|
||||
return this.scopedInstances.get(serviceType) as T;
|
||||
if (this._scopedInstances.has(serviceType)) {
|
||||
return this._scopedInstances.get(serviceType) as T;
|
||||
}
|
||||
const scopedInstance = factory ? factory(this) : this.createInstance(implementationType);
|
||||
this.scopedInstances.set(serviceType, scopedInstance);
|
||||
this._scopedInstances.set(serviceType, scopedInstance);
|
||||
return scopedInstance;
|
||||
}
|
||||
case 'transient':
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ export interface IServiceCollection {
|
||||
addScoped<T>(serviceType: ServiceIdentifier<T>, factory: ServiceFactory<T>): IServiceCollection;
|
||||
addTransient<T>(serviceType: ServiceIdentifier<T>, implementationType: Constructor<T>): IServiceCollection;
|
||||
addTransient<T>(serviceType: ServiceIdentifier<T>, factory: ServiceFactory<T>): IServiceCollection;
|
||||
buildServiceProvider(): IServiceProvider;
|
||||
build(): IServiceProvider;
|
||||
}
|
||||
|
||||
export interface IServiceProvider {
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { createServiceIdentifier, ServiceCollection } from '../src';
|
||||
|
||||
describe('ServiceCollection', () => {
|
||||
it('should add a singleton service', () => {
|
||||
class TestService {}
|
||||
const serviceIdentifier = createServiceIdentifier();
|
||||
|
||||
const collection = new ServiceCollection();
|
||||
collection.addSingleton(serviceIdentifier, TestService);
|
||||
|
||||
const provider = collection.build();
|
||||
const serviceInstance1 = provider.getService(serviceIdentifier);
|
||||
const serviceInstance2 = provider.getService(serviceIdentifier);
|
||||
|
||||
expect(serviceInstance1).toBe(serviceInstance2);
|
||||
});
|
||||
|
||||
it('should add a scoped service', () => {
|
||||
class TestService {}
|
||||
const serviceIdentifier = createServiceIdentifier();
|
||||
|
||||
const collection = new ServiceCollection();
|
||||
collection.addScoped(serviceIdentifier, TestService);
|
||||
|
||||
const provider = collection.build();
|
||||
const scope1 = provider.createScope();
|
||||
const scope2 = provider.createScope();
|
||||
|
||||
const serviceInstance1 = scope1.serviceProvider.getService(serviceIdentifier);
|
||||
const serviceInstance2 = scope2.serviceProvider.getService(serviceIdentifier);
|
||||
|
||||
expect(serviceInstance1).not.toBe(serviceInstance2);
|
||||
});
|
||||
|
||||
it('should add a transient service', () => {
|
||||
class TestService {}
|
||||
const serviceIdentifier = createServiceIdentifier();
|
||||
|
||||
const collection = new ServiceCollection();
|
||||
collection.addTransient(serviceIdentifier, TestService);
|
||||
|
||||
const provider = collection.build();
|
||||
|
||||
const serviceInstance1 = provider.getService(serviceIdentifier);
|
||||
const serviceInstance2 = provider.getService(serviceIdentifier);
|
||||
|
||||
expect(serviceInstance1).not.toBe(serviceInstance2);
|
||||
});
|
||||
|
||||
it('should add a single service with a factory', () => {
|
||||
class TestService {}
|
||||
const serviceIdentifier = createServiceIdentifier();
|
||||
|
||||
const collection = new ServiceCollection();
|
||||
collection.addSingleton(serviceIdentifier, () => new TestService());
|
||||
|
||||
const provider = collection.build();
|
||||
const serviceInstance1 = provider.getService(serviceIdentifier);
|
||||
const serviceInstance2 = provider.getService(serviceIdentifier);
|
||||
|
||||
expect(serviceInstance1).toBe(serviceInstance2);
|
||||
});
|
||||
|
||||
it('should add a scoped service with a factory', () => {
|
||||
class TestService {}
|
||||
const serviceIdentifier = createServiceIdentifier();
|
||||
|
||||
const collection = new ServiceCollection();
|
||||
collection.addScoped(serviceIdentifier, () => new TestService());
|
||||
|
||||
const provider = collection.build();
|
||||
const scope1 = provider.createScope();
|
||||
const scope2 = provider.createScope();
|
||||
|
||||
const serviceInstance1 = scope1.serviceProvider.getService(serviceIdentifier);
|
||||
const serviceInstance2 = scope2.serviceProvider.getService(serviceIdentifier);
|
||||
|
||||
expect(serviceInstance1).not.toBe(serviceInstance2);
|
||||
});
|
||||
|
||||
|
||||
it('should add a transient service with a factory', () => {
|
||||
class TestService {}
|
||||
const serviceIdentifier = createServiceIdentifier();
|
||||
|
||||
const collection = new ServiceCollection();
|
||||
collection.addTransient(serviceIdentifier, () => new TestService());
|
||||
|
||||
const provider = collection.build();
|
||||
|
||||
const serviceInstance1 = provider.getService(serviceIdentifier);
|
||||
const serviceInstance2 = provider.getService(serviceIdentifier);
|
||||
|
||||
expect(serviceInstance1).not.toBe(serviceInstance2);
|
||||
});
|
||||
});
|
||||
@@ -1,11 +1,11 @@
|
||||
import { describe, expect, it, test } from 'vitest';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { injectable, ServiceProvider } from '../src';
|
||||
import { createServiceIdentifier, ServiceLifetime } from '../src/types';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { inject } from '../src';
|
||||
|
||||
describe('ServiceProvider', () => {
|
||||
it('it should throw an error when service is not registered', () => {
|
||||
it('should throw an error when service is not registered', () => {
|
||||
type IService = object;
|
||||
const serviceIdentifier = createServiceIdentifier<IService>();
|
||||
const serviceProvider = new ServiceProvider(new Map());
|
||||
@@ -15,7 +15,7 @@ describe('ServiceProvider', () => {
|
||||
}).toThrowError();
|
||||
});
|
||||
|
||||
it('it should resolve singleton service', () => {
|
||||
it('should resolve singleton service', () => {
|
||||
type IService = object;
|
||||
const serviceIdentifier = createServiceIdentifier<IService>();
|
||||
const serviceProvider = new ServiceProvider(
|
||||
@@ -37,7 +37,7 @@ describe('ServiceProvider', () => {
|
||||
expect(service1).toBe(service2);
|
||||
});
|
||||
|
||||
it('it should resolve singleton service using factory', () => {
|
||||
it('should resolve singleton service using factory', () => {
|
||||
type IService = object;
|
||||
const serviceIdentifier = createServiceIdentifier<IService>();
|
||||
const serviceProvider = new ServiceProvider(
|
||||
@@ -60,7 +60,7 @@ describe('ServiceProvider', () => {
|
||||
expect(service1).toBe(service2);
|
||||
});
|
||||
|
||||
it('it should resolve scoped service', () => {
|
||||
it('should resolve scoped service', () => {
|
||||
type IService = object;
|
||||
const serviceIdentifier = createServiceIdentifier<IService>();
|
||||
const serviceProvider = new ServiceProvider(
|
||||
@@ -85,7 +85,7 @@ describe('ServiceProvider', () => {
|
||||
expect(service1).not.toBe(service2);
|
||||
});
|
||||
|
||||
it('it should resolve scoped service using factory', () => {
|
||||
it('should resolve scoped service using factory', () => {
|
||||
type IService = object;
|
||||
const serviceIdentifier = createServiceIdentifier<IService>();
|
||||
const serviceProvider = new ServiceProvider(
|
||||
@@ -111,7 +111,7 @@ describe('ServiceProvider', () => {
|
||||
expect(service1).not.toBe(service2);
|
||||
});
|
||||
|
||||
it('it should resolve transient service', () => {
|
||||
it('should resolve transient service', () => {
|
||||
type IService = object;
|
||||
const serviceIdentifier = createServiceIdentifier<IService>();
|
||||
const serviceProvider = new ServiceProvider(
|
||||
@@ -133,7 +133,7 @@ describe('ServiceProvider', () => {
|
||||
expect(service1).not.toBe(service2);
|
||||
});
|
||||
|
||||
it('it should resolve transient service using factory', () => {
|
||||
it('should resolve transient service using factory', () => {
|
||||
type IService = object;
|
||||
const serviceIdentifier = createServiceIdentifier<IService>();
|
||||
const serviceProvider = new ServiceProvider(
|
||||
@@ -156,7 +156,7 @@ describe('ServiceProvider', () => {
|
||||
expect(service1).not.toBe(service2);
|
||||
});
|
||||
|
||||
it('it should resolve same singleton instance across scopes', () => {
|
||||
it('should resolve same singleton instance across scopes', () => {
|
||||
type IService = {
|
||||
id: string;
|
||||
};
|
||||
@@ -193,7 +193,7 @@ describe('ServiceProvider', () => {
|
||||
expect(service1.id).toBe(service2.id);
|
||||
});
|
||||
|
||||
it('it should resolve solve injected dependencies', () => {
|
||||
it('should resolve solve injected dependencies', () => {
|
||||
interface IUserRepository {
|
||||
getUser(): string;
|
||||
}
|
||||
@@ -253,7 +253,7 @@ describe('ServiceProvider', () => {
|
||||
expect(user).toBe('John Doe');
|
||||
});
|
||||
|
||||
test('it should throw an error when service has invalid lifetime', () => {
|
||||
it('should throw an error when service has invalid lifetime', () => {
|
||||
type IService = object;
|
||||
|
||||
const serviceIdentifier = createServiceIdentifier<IService>();
|
||||
@@ -276,7 +276,7 @@ describe('ServiceProvider', () => {
|
||||
}).toThrowError();
|
||||
});
|
||||
|
||||
test('it should resolve same scoped instance in the same scope', () => {
|
||||
it('should resolve same scoped instance in the same scope', () => {
|
||||
type IService = {
|
||||
id: string;
|
||||
};
|
||||
@@ -312,7 +312,7 @@ describe('ServiceProvider', () => {
|
||||
expect(service1).toBe(service2);
|
||||
});
|
||||
|
||||
test('it should clear the scoped instances on dispose from provider', () => {
|
||||
it('should clear the scoped instances on dispose from provider', () => {
|
||||
type IService = {
|
||||
id: string;
|
||||
};
|
||||
@@ -349,7 +349,7 @@ describe('ServiceProvider', () => {
|
||||
expect(initialService).not.toBe(newService);
|
||||
});
|
||||
|
||||
test('it should clear the scoped instances on dispose from a scope', () => {
|
||||
it('should clear the scoped instances on dispose from a scope', () => {
|
||||
type IService = {
|
||||
id: string;
|
||||
};
|
||||
@@ -388,7 +388,7 @@ describe('ServiceProvider', () => {
|
||||
expect(initialService).not.toBe(newService);
|
||||
});
|
||||
|
||||
test('it should not clear the scoped instances on dispose from a scope when the provider is disposed', () => {
|
||||
it('should not clear the scoped instances on dispose from a scope when the provider is disposed', () => {
|
||||
type IService = {
|
||||
id: string;
|
||||
};
|
||||
@@ -427,7 +427,7 @@ describe('ServiceProvider', () => {
|
||||
expect(initialService).toBe(newService);
|
||||
});
|
||||
|
||||
test("it should not clear the scoped instances on dispose from another scope when one scope is disposed", () => {
|
||||
it("should not clear the scoped instances on dispose from another scope when one scope is disposed", () => {
|
||||
type IService = {
|
||||
id: string;
|
||||
};
|
||||
@@ -470,7 +470,7 @@ describe('ServiceProvider', () => {
|
||||
expect(initialService2).toBe(newService2);
|
||||
});
|
||||
|
||||
test('it should throw error when dependency is not registered', () => {
|
||||
it('should throw error when dependency is not registered', () => {
|
||||
interface IUserRepository {
|
||||
getUser(): string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user