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