feat: implement service provider

This commit is contained in:
Stevan Freeborn
2025-05-12 22:23:31 -05:00
parent 153dadcfc8
commit eda06cc9d4
10 changed files with 996 additions and 3 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
build/
dist/
node_modules/
coverage/
+313
View File
@@ -8,7 +8,11 @@
"name": "@stevanfreeborn/netdi",
"version": "0.0.0",
"license": "MIT",
"dependencies": {
"reflect-metadata": "^0.2.2"
},
"devDependencies": {
"@swc/core": "^1.11.24",
"@types/node": "^22.15.17",
"@vitest/coverage-istanbul": "^3.1.3",
"@vitest/ui": "^3.1.3",
@@ -17,6 +21,7 @@
"prettier": "^3.5.3",
"typescript": "^5.8.3",
"typescript-eslint": "^8.32.0",
"unplugin-swc": "^1.5.2",
"vitest": "^3.1.3"
}
},
@@ -1095,6 +1100,36 @@
"dev": true,
"license": "MIT"
},
"node_modules/@rollup/pluginutils": {
"version": "5.1.4",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz",
"integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "^1.0.0",
"estree-walker": "^2.0.2",
"picomatch": "^4.0.2"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rollup/pluginutils/node_modules/estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"dev": true,
"license": "MIT"
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz",
@@ -1375,6 +1410,232 @@
"win32"
]
},
"node_modules/@swc/core": {
"version": "1.11.24",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.11.24.tgz",
"integrity": "sha512-MaQEIpfcEMzx3VWWopbofKJvaraqmL6HbLlw2bFZ7qYqYw3rkhM0cQVEgyzbHtTWwCwPMFZSC2DUbhlZgrMfLg==",
"dev": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@swc/counter": "^0.1.3",
"@swc/types": "^0.1.21"
},
"engines": {
"node": ">=10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/swc"
},
"optionalDependencies": {
"@swc/core-darwin-arm64": "1.11.24",
"@swc/core-darwin-x64": "1.11.24",
"@swc/core-linux-arm-gnueabihf": "1.11.24",
"@swc/core-linux-arm64-gnu": "1.11.24",
"@swc/core-linux-arm64-musl": "1.11.24",
"@swc/core-linux-x64-gnu": "1.11.24",
"@swc/core-linux-x64-musl": "1.11.24",
"@swc/core-win32-arm64-msvc": "1.11.24",
"@swc/core-win32-ia32-msvc": "1.11.24",
"@swc/core-win32-x64-msvc": "1.11.24"
},
"peerDependencies": {
"@swc/helpers": ">=0.5.17"
},
"peerDependenciesMeta": {
"@swc/helpers": {
"optional": true
}
}
},
"node_modules/@swc/core-darwin-arm64": {
"version": "1.11.24",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.11.24.tgz",
"integrity": "sha512-dhtVj0PC1APOF4fl5qT2neGjRLgHAAYfiVP8poJelhzhB/318bO+QCFWAiimcDoyMgpCXOhTp757gnoJJrheWA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-darwin-x64": {
"version": "1.11.24",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.11.24.tgz",
"integrity": "sha512-H/3cPs8uxcj2Fe3SoLlofN5JG6Ny5bl8DuZ6Yc2wr7gQFBmyBkbZEz+sPVgsID7IXuz7vTP95kMm1VL74SO5AQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-arm-gnueabihf": {
"version": "1.11.24",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.11.24.tgz",
"integrity": "sha512-PHJgWEpCsLo/NGj+A2lXZ2mgGjsr96ULNW3+T3Bj2KTc8XtMUkE8tmY2Da20ItZOvPNC/69KroU7edyo1Flfbw==",
"cpu": [
"arm"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-arm64-gnu": {
"version": "1.11.24",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.11.24.tgz",
"integrity": "sha512-C2FJb08+n5SD4CYWCTZx1uR88BN41ZieoHvI8A55hfVf2woT8+6ZiBzt74qW2g+ntZ535Jts5VwXAKdu41HpBg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-arm64-musl": {
"version": "1.11.24",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.11.24.tgz",
"integrity": "sha512-ypXLIdszRo0re7PNNaXN0+2lD454G8l9LPK/rbfRXnhLWDBPURxzKlLlU/YGd2zP98wPcVooMmegRSNOKfvErw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-x64-gnu": {
"version": "1.11.24",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.11.24.tgz",
"integrity": "sha512-IM7d+STVZD48zxcgo69L0yYptfhaaE9cMZ+9OoMxirNafhKKXwoZuufol1+alEFKc+Wbwp+aUPe/DeWC/Lh3dg==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-x64-musl": {
"version": "1.11.24",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.11.24.tgz",
"integrity": "sha512-DZByJaMVzSfjQKKQn3cqSeqwy6lpMaQDQQ4HPlch9FWtDx/dLcpdIhxssqZXcR2rhaQVIaRQsCqwV6orSDGAGw==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-win32-arm64-msvc": {
"version": "1.11.24",
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.11.24.tgz",
"integrity": "sha512-Q64Ytn23y9aVDKN5iryFi8mRgyHw3/kyjTjT4qFCa8AEb5sGUuSj//AUZ6c0J7hQKMHlg9do5Etvoe61V98/JQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-win32-ia32-msvc": {
"version": "1.11.24",
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.11.24.tgz",
"integrity": "sha512-9pKLIisE/Hh2vJhGIPvSoTK4uBSPxNVyXHmOrtdDot4E1FUUI74Vi8tFdlwNbaj8/vusVnb8xPXsxF1uB0VgiQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-win32-x64-msvc": {
"version": "1.11.24",
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.11.24.tgz",
"integrity": "sha512-sybnXtOsdB+XvzVFlBVGgRHLqp3yRpHK7CrmpuDKszhj/QhmsaZzY/GHSeALlMtLup13M0gqbcQvsTNlAHTg3w==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/counter": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/@swc/types": {
"version": "0.1.21",
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.21.tgz",
"integrity": "sha512-2YEtj5HJVbKivud9N4bpPBAyZhj4S2Ipe5LkUG94alTpr7in/GU/EARgPAd3BwU+YOmFVJC2+kjqhGRi3r0ZpQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@swc/counter": "^0.1.3"
}
},
"node_modules/@types/estree": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
@@ -3391,6 +3652,16 @@
"node": ">= 0.8.0"
}
},
"node_modules/load-tsconfig": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz",
"integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
}
},
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -4014,6 +4285,12 @@
"node": ">= 0.8"
}
},
"node_modules/reflect-metadata": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
"integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==",
"license": "Apache-2.0"
},
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -4731,6 +5008,35 @@
"node": ">= 0.8"
}
},
"node_modules/unplugin": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.16.1.tgz",
"integrity": "sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==",
"dev": true,
"license": "MIT",
"dependencies": {
"acorn": "^8.14.0",
"webpack-virtual-modules": "^0.6.2"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/unplugin-swc": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/unplugin-swc/-/unplugin-swc-1.5.2.tgz",
"integrity": "sha512-bf8DJO8lD1wpnwFglQpVH2XEaFfVsSU5C7yFyLwGT1gxskPtejlDeuttKxjtmHTSqrDsQrK0FCFdhw3Ny+K7hA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rollup/pluginutils": "^5.1.0",
"load-tsconfig": "^0.2.5",
"unplugin": "^1.11.0"
},
"peerDependencies": {
"@swc/core": "^1.2.108"
}
},
"node_modules/update-browserslist-db": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
@@ -4951,6 +5257,13 @@
}
}
},
"node_modules/webpack-virtual-modules": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
"integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
"dev": true,
"license": "MIT"
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+5
View File
@@ -47,6 +47,7 @@
"license": "MIT",
"type": "module",
"devDependencies": {
"@swc/core": "^1.11.24",
"@types/node": "^22.15.17",
"@vitest/coverage-istanbul": "^3.1.3",
"@vitest/ui": "^3.1.3",
@@ -55,6 +56,10 @@
"prettier": "^3.5.3",
"typescript": "^5.8.3",
"typescript-eslint": "^8.32.0",
"unplugin-swc": "^1.5.2",
"vitest": "^3.1.3"
},
"dependencies": {
"reflect-metadata": "^0.2.2"
}
}
+15
View File
@@ -0,0 +1,15 @@
import type { ServiceIdentifier } from './types.js';
export function inject<T>(serviceType: ServiceIdentifier<T>): ParameterDecorator {
// eslint-disable-next-line @typescript-eslint/no-wrapper-object-types
return (target: Object, _: string | symbol | undefined, parameterIndex: number) => {
Reflect.defineMetadata('di:paramtypes', serviceType, target, parameterIndex.toString());
};
}
export function injectable(): ClassDecorator {
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
return (target: Function) => {
Reflect.defineMetadata('di:injectable', true, target);
};
}
+5 -1
View File
@@ -1 +1,5 @@
console.log('netdi');
import "reflect-metadata";
export * from './serviceProvider.js';
export * from './types.js';
export * from './decorators.js';
+106
View File
@@ -0,0 +1,106 @@
import type {
IServiceProvider,
IServiceScope,
ServiceDescriptor,
ServiceIdentifier,
Constructor,
} from './types.js';
export class ServiceScope implements IServiceScope {
public readonly serviceProvider: IServiceProvider;
constructor(serviceProvider: IServiceProvider) {
this.serviceProvider = serviceProvider;
}
dispose(): void {
this.serviceProvider.dispose();
}
}
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();
constructor(
descriptors: Map<ServiceIdentifier<unknown>, ServiceDescriptor<unknown>>,
parent?: ServiceProvider,
) {
this.descriptors = descriptors;
if (parent) {
parent.singletonInstances.forEach((value, key) => {
this.singletonInstances.set(key, value);
});
}
for (const descriptor of descriptors.values()) {
if (descriptor.lifetime === 'singleton') {
this.resolveService(descriptor);
}
}
}
getService<T>(serviceType: ServiceIdentifier<T>): T {
const descriptor = this.descriptors.get(serviceType);
if (!descriptor) {
throw new Error(`Service of type ${serviceType.toString()} is not registered.`);
}
return this.resolveService(descriptor as ServiceDescriptor<T>);
}
createScope(): IServiceScope {
const scopedProvider = new ServiceProvider(this.descriptors, this);
return new ServiceScope(scopedProvider);
}
dispose(): void {
this.scopedInstances.clear();
}
private resolveService<T>(descriptor: ServiceDescriptor<T>): T {
const { serviceType, implementationType, lifetime, factory } = descriptor;
switch (lifetime) {
case 'singleton': {
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);
return singletonInstance;
}
case 'scoped': {
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);
return scopedInstance;
}
case 'transient':
return factory ? factory(this) : this.createInstance(implementationType);
default:
throw new Error(`Unknown service lifetime: ${lifetime}`);
}
}
private createInstance<T>(ctor: Constructor<T>): T {
const paramTypes = Reflect.getMetadata('design:paramtypes', ctor) ?? [];
const params = paramTypes.map((_: unknown, index: number) => {
const serviceType = Reflect.getMetadata(
'di:paramtypes',
ctor,
index.toString(),
) as ServiceIdentifier<unknown>;
return this.getService(serviceType);
});
return new ctor(...params);
}
}
+40
View File
@@ -0,0 +1,40 @@
export type ServiceLifetime = 'singleton' | 'scoped' | 'transient';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type Constructor<T> = new (...args: any[]) => T;
export type ServiceIdentifier<T = unknown> = symbol & { __brand: T };
export type ServiceFactory<T> = (provider: IServiceProvider) => T;
export type ServiceDescriptor<T = unknown> = {
serviceType: ServiceIdentifier<T>;
implementationType: Constructor<T>;
lifetime: ServiceLifetime;
factory?: ServiceFactory<T>;
};
export function createServiceIdentifier<T>(): ServiceIdentifier<T> {
return Symbol() as ServiceIdentifier<T>;
}
export interface IServiceCollection {
addSingleton<T>(serviceType: ServiceIdentifier<T>, implementationType: Constructor<T>): IServiceCollection;
addSingleton<T>(serviceType: ServiceIdentifier<T>, factory: ServiceFactory<T>): IServiceCollection;
addScoped<T>(serviceType: ServiceIdentifier<T>, implementationType: Constructor<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>, factory: ServiceFactory<T>): IServiceCollection;
buildServiceProvider(): IServiceProvider;
}
export interface IServiceProvider {
getService<T>(serviceType: ServiceIdentifier<T>): T;
createScope(): IServiceScope;
dispose(): void;
}
export interface IServiceScope {
serviceProvider: IServiceProvider;
dispose(): void;
}
-1
View File
@@ -1 +0,0 @@
console.log('netdi tests');
+509
View File
@@ -0,0 +1,509 @@
import { describe, expect, it, test } 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', () => {
type IService = object;
const serviceIdentifier = createServiceIdentifier<IService>();
const serviceProvider = new ServiceProvider(new Map());
expect(() => {
serviceProvider.getService(serviceIdentifier);
}).toThrowError();
});
it('it should resolve singleton service', () => {
type IService = object;
const serviceIdentifier = createServiceIdentifier<IService>();
const serviceProvider = new ServiceProvider(
new Map([
[
serviceIdentifier,
{
serviceType: serviceIdentifier,
implementationType: Object,
lifetime: 'singleton',
},
],
]),
);
const service1 = serviceProvider.getService(serviceIdentifier);
const service2 = serviceProvider.getService(serviceIdentifier);
expect(service1).toBe(service2);
});
it('it should resolve singleton service using factory', () => {
type IService = object;
const serviceIdentifier = createServiceIdentifier<IService>();
const serviceProvider = new ServiceProvider(
new Map([
[
serviceIdentifier,
{
serviceType: serviceIdentifier,
implementationType: Object,
lifetime: 'singleton',
factory: () => new Object(),
},
],
]),
);
const service1 = serviceProvider.getService(serviceIdentifier);
const service2 = serviceProvider.getService(serviceIdentifier);
expect(service1).toBe(service2);
});
it('it should resolve scoped service', () => {
type IService = object;
const serviceIdentifier = createServiceIdentifier<IService>();
const serviceProvider = new ServiceProvider(
new Map([
[
serviceIdentifier,
{
serviceType: serviceIdentifier,
implementationType: Object,
lifetime: 'scoped',
},
],
]),
);
const scope1 = serviceProvider.createScope();
const scope2 = serviceProvider.createScope();
const service1 = scope1.serviceProvider.getService(serviceIdentifier);
const service2 = scope2.serviceProvider.getService(serviceIdentifier);
expect(service1).not.toBe(service2);
});
it('it should resolve scoped service using factory', () => {
type IService = object;
const serviceIdentifier = createServiceIdentifier<IService>();
const serviceProvider = new ServiceProvider(
new Map([
[
serviceIdentifier,
{
serviceType: serviceIdentifier,
implementationType: Object,
lifetime: 'scoped',
factory: () => new Object(),
},
],
]),
);
const scope1 = serviceProvider.createScope();
const scope2 = serviceProvider.createScope();
const service1 = scope1.serviceProvider.getService(serviceIdentifier);
const service2 = scope2.serviceProvider.getService(serviceIdentifier);
expect(service1).not.toBe(service2);
});
it('it should resolve transient service', () => {
type IService = object;
const serviceIdentifier = createServiceIdentifier<IService>();
const serviceProvider = new ServiceProvider(
new Map([
[
serviceIdentifier,
{
serviceType: serviceIdentifier,
implementationType: Object,
lifetime: 'transient',
},
],
]),
);
const service1 = serviceProvider.getService(serviceIdentifier);
const service2 = serviceProvider.getService(serviceIdentifier);
expect(service1).not.toBe(service2);
});
it('it should resolve transient service using factory', () => {
type IService = object;
const serviceIdentifier = createServiceIdentifier<IService>();
const serviceProvider = new ServiceProvider(
new Map([
[
serviceIdentifier,
{
serviceType: serviceIdentifier,
implementationType: Object,
lifetime: 'transient',
factory: () => new Object(),
},
],
]),
);
const service1 = serviceProvider.getService(serviceIdentifier);
const service2 = serviceProvider.getService(serviceIdentifier);
expect(service1).not.toBe(service2);
});
it('it should resolve same singleton instance across scopes', () => {
type IService = {
id: string;
};
class Service implements IService {
id: string;
constructor() {
this.id = randomUUID();
}
}
const serviceIdentifier = createServiceIdentifier<IService>();
const serviceProvider = new ServiceProvider(
new Map([
[
serviceIdentifier,
{
serviceType: serviceIdentifier,
implementationType: Service,
lifetime: 'singleton',
},
],
]),
);
const scope1 = serviceProvider.createScope();
const scope2 = serviceProvider.createScope();
const service1 = scope1.serviceProvider.getService(serviceIdentifier);
const service2 = scope2.serviceProvider.getService(serviceIdentifier);
expect(service1.id).toBe(service2.id);
});
it('it should resolve solve injected dependencies', () => {
interface IUserRepository {
getUser(): string;
}
@injectable()
class UserRepository implements IUserRepository {
getUser(): string {
return 'John Doe';
}
}
const userRepositoryIdentifier = createServiceIdentifier<IUserRepository>();
interface IUserService {
getUser(): string;
}
class UserService implements IUserService {
private userRepository: IUserRepository;
constructor(@inject(userRepositoryIdentifier) userRepository: IUserRepository) {
this.userRepository = userRepository;
}
getUser(): string {
return this.userRepository.getUser();
}
}
const userServiceIdentifier = createServiceIdentifier<IUserService>();
const serviceProvider = new ServiceProvider(
new Map([
[
userRepositoryIdentifier,
{
serviceType: userRepositoryIdentifier,
implementationType: UserRepository,
lifetime: 'singleton',
},
],
[
userServiceIdentifier,
{
serviceType: userServiceIdentifier,
implementationType: UserService,
lifetime: 'singleton',
},
],
]),
);
const userService = serviceProvider.getService(userServiceIdentifier);
const user = userService.getUser();
expect(user).toBe('John Doe');
});
test('it should throw an error when service has invalid lifetime', () => {
type IService = object;
const serviceIdentifier = createServiceIdentifier<IService>();
const serviceProvider = new ServiceProvider(
new Map([
[
serviceIdentifier,
{
serviceType: serviceIdentifier,
implementationType: Object,
lifetime: 'invalid' as ServiceLifetime,
},
],
]),
);
expect(() => {
serviceProvider.getService(serviceIdentifier);
}).toThrowError();
});
test('it should resolve same scoped instance in the same scope', () => {
type IService = {
id: string;
};
class Service implements IService {
id: string;
constructor() {
this.id = randomUUID();
}
}
const serviceIdentifier = createServiceIdentifier<IService>();
const serviceProvider = new ServiceProvider(
new Map([
[
serviceIdentifier,
{
serviceType: serviceIdentifier,
implementationType: Service,
lifetime: 'scoped',
},
],
]),
);
const scope = serviceProvider.createScope();
const service1 = scope.serviceProvider.getService(serviceIdentifier);
const service2 = scope.serviceProvider.getService(serviceIdentifier);
expect(service1).toBe(service2);
});
test('it should clear the scoped instances on dispose from provider', () => {
type IService = {
id: string;
};
class Service implements IService {
id: string;
constructor() {
this.id = randomUUID();
}
}
const serviceIdentifier = createServiceIdentifier<IService>();
const serviceProvider = new ServiceProvider(
new Map([
[
serviceIdentifier,
{
serviceType: serviceIdentifier,
implementationType: Service,
lifetime: 'scoped',
},
],
]),
);
const initialService = serviceProvider.getService(serviceIdentifier);
serviceProvider.dispose();
const newService = serviceProvider.getService(serviceIdentifier);
expect(initialService).not.toBe(newService);
});
test('it should clear the scoped instances on dispose from a scope', () => {
type IService = {
id: string;
};
class Service implements IService {
id: string;
constructor() {
this.id = randomUUID();
}
}
const serviceIdentifier = createServiceIdentifier<IService>();
const serviceProvider = new ServiceProvider(
new Map([
[
serviceIdentifier,
{
serviceType: serviceIdentifier,
implementationType: Service,
lifetime: 'scoped',
},
],
]),
);
const scope = serviceProvider.createScope();
const initialService = scope.serviceProvider.getService(serviceIdentifier);
scope.dispose();
const newService = scope.serviceProvider.getService(serviceIdentifier);
expect(initialService).not.toBe(newService);
});
test('it should not clear the scoped instances on dispose from a scope when the provider is disposed', () => {
type IService = {
id: string;
};
class Service implements IService {
id: string;
constructor() {
this.id = randomUUID();
}
}
const serviceIdentifier = createServiceIdentifier<IService>();
const serviceProvider = new ServiceProvider(
new Map([
[
serviceIdentifier,
{
serviceType: serviceIdentifier,
implementationType: Service,
lifetime: 'scoped',
},
],
]),
);
const scope = serviceProvider.createScope();
const initialService = scope.serviceProvider.getService(serviceIdentifier);
serviceProvider.dispose();
const newService = scope.serviceProvider.getService(serviceIdentifier);
expect(initialService).toBe(newService);
});
test("it should not clear the scoped instances on dispose from another scope when one scope is disposed", () => {
type IService = {
id: string;
};
class Service implements IService {
id: string;
constructor() {
this.id = randomUUID();
}
}
const serviceIdentifier = createServiceIdentifier<IService>();
const serviceProvider = new ServiceProvider(
new Map([
[
serviceIdentifier,
{
serviceType: serviceIdentifier,
implementationType: Service,
lifetime: 'scoped',
},
],
]),
);
const scope1 = serviceProvider.createScope();
const scope2 = serviceProvider.createScope();
const initialService1 = scope1.serviceProvider.getService(serviceIdentifier);
const initialService2 = scope2.serviceProvider.getService(serviceIdentifier);
scope1.dispose();
const newService1 = scope1.serviceProvider.getService(serviceIdentifier);
const newService2 = scope2.serviceProvider.getService(serviceIdentifier);
expect(initialService1).not.toBe(newService1);
expect(initialService2).toBe(newService2);
});
test('it should throw error when dependency is not registered', () => {
interface IUserRepository {
getUser(): string;
}
class UserService {
private userRepository: IUserRepository;
constructor(@inject('IUserRepository') userRepository: IUserRepository) {
this.userRepository = userRepository;
}
getUser(): string {
return this.userRepository.getUser();
}
}
const userServiceIdentifier = createServiceIdentifier<UserService>();
const serviceProvider = new ServiceProvider(
new Map([
[
userServiceIdentifier,
{
serviceType: userServiceIdentifier,
implementationType: UserService,
lifetime: 'scoped',
},
],
]),
);
expect(() => {
serviceProvider.getService(userServiceIdentifier);
}).toThrowError();
});
});
+2
View File
@@ -1,4 +1,5 @@
import { defineConfig } from 'vitest/config';
import swc from 'unplugin-swc';
export default defineConfig({
test: {
@@ -10,4 +11,5 @@ export default defineConfig({
exclude: ['**/src/index.ts', '**/src/server.ts'],
},
},
plugins: [swc.vite()],
});