Skip to content

Commit

Permalink
Merge pull request #9 from AnasBoulmane/performance
Browse files Browse the repository at this point in the history
Performance: improve set and get performances:
 * make the service id mandatory
 * improve testing coverages
 * migrate jest configuration Using the CLI ts-jest config:migrate
  • Loading branch information
AnasBoulmane committed Oct 24, 2019
2 parents 230eada + 179ef1b commit 2d914c0
Show file tree
Hide file tree
Showing 13 changed files with 293 additions and 86 deletions.
20 changes: 13 additions & 7 deletions jest.config.js
@@ -1,13 +1,19 @@
module.exports = {
globals: {
"ts-jest": {
tsConfigFile: `${__dirname}/tsconfig.json`,
'ts-jest': {
tsConfig: `${__dirname}/tsconfig.json`,
},
},
moduleFileExtensions: ["ts", "js"],
moduleFileExtensions: [
'js',
'ts',
],
transform: {
"^.+\\.(ts|tsx)$": "./node_modules/ts-jest/preprocessor.js",
'^.+\\.(ts|tsx)$': 'ts-jest',
},
testMatch: ["**/test/**/*.spec.(ts|js)"],
testEnvironment: "node",
};
testMatch: [
'**/test/**/*.spec.(ts|js)',
],
testEnvironment: 'node',
preset: 'ts-jest',
}
4 changes: 3 additions & 1 deletion package.json
@@ -1,7 +1,7 @@
{
"name": "@express.ts/container",
"version": "0.8.0",
"description": "test package",
"description": "Simple yet powerful implementation of the Inversion of Control (IoC) principle. IoC is also known as dependency injection (DI). based on TypeDI",
"author": "aboulmane <anassboulmane@gmail.com>",
"main": "dist/index",
"license": "ISC",
Expand All @@ -25,10 +25,12 @@
"devDependencies": {
"@types/chai": "^4.2.3",
"@types/jest": "^24.0.15",
"@types/node": "^12.11.1",
"@types/sinon-chai": "^3.2.3",
"chai": "^4.2.0",
"coveralls": "^3.0.6",
"jest": "^24.9.0",
"perf_hooks": "^0.0.1",
"prettier": "^1.18.2",
"prettier-tslint": "^0.4.2",
"reflect-metadata": "^0.1.13",
Expand Down
4 changes: 2 additions & 2 deletions sample/sample2-inject/app.ts
@@ -1,6 +1,6 @@
import "reflect-metadata";
import { Container } from "../../src/index";
import { Container } from "../../src";
import { CoffeeMaker } from "./CoffeeMaker";

const coffeeMaker = Container.get(CoffeeMaker);
const coffeeMaker = Container.get<CoffeeMaker>(CoffeeMaker);
coffeeMaker.make();
13 changes: 11 additions & 2 deletions src/Container.ts
Expand Up @@ -25,7 +25,7 @@ export class Container {
/**
* All registered handlers.
*/
static readonly handlers: Handler[] = [];
static readonly handlers: Map<any, Handler[]> = new Map<any, Handler[]>();

// -------------------------------------------------------------------------
// Public Static Methods
Expand Down Expand Up @@ -123,7 +123,16 @@ export class Container {
* Registers a new handler.
*/
static registerHandler (handler: Handler): Container {
this.handlers.push(handler);
const id =
handler.propertyName && typeof handler.object !== "string" && !(handler.object instanceof Token)
? handler.object.constructor
: handler.object;
const handlers = this.handlers.get(id);
if (handlers) {
handlers.push(handler);
} else {
this.handlers.set(id, [handler]);
}
return this;
}

Expand Down
102 changes: 42 additions & 60 deletions src/ContainerInstance.ts
Expand Up @@ -26,7 +26,8 @@ export class ContainerInstance {
/**
* All registered services.
*/
private services: Array<ServiceMetadata<any, any>> = [];
private services: Map<ServiceIdentifier<any>, ServiceMetadata<any, any>> = new Map();
private groupedServices: Map<ServiceIdentifier<any>, Array<ServiceMetadata<any, any>>> = new Map();

// -------------------------------------------------------------------------
// Constructor
Expand Down Expand Up @@ -63,6 +64,7 @@ export class ContainerInstance {
const globalContainer = Container.of(undefined);
const service = globalContainer.findService(identifier);
const scopedService = this.findService(identifier);
const groupedService = this.groupedServices.get(identifier);

if (service && service.global === true) {
return this.getServiceValue(identifier, service);
Expand All @@ -80,6 +82,10 @@ export class ContainerInstance {
return value;
}

if (groupedService && groupedService.length) {
return this.getServiceValue(identifier, groupedService[0]);
}

return this.getServiceValue(identifier, service);
}

Expand All @@ -88,7 +94,11 @@ export class ContainerInstance {
* Used when service defined with multiple: true flag.
*/
getMany<T> (id: string | Token<T>): T[] {
return this.filterServices(id).map((service) => this.getServiceValue(id, service));
try {
return this.groupedServices.get(id).map((service) => this.getServiceValue(id, service));
} catch (e) {
throw new ServiceNotFoundError(id);
}
}

/**
Expand All @@ -105,32 +115,42 @@ export class ContainerInstance {
identifierOrServiceMetadata: ServiceIdentifier | ServiceMetadata<any, any> | (Array<ServiceMetadata<any, any>>),
value?: any,
): this {
let newService: ServiceMetadata<any, any> = identifierOrServiceMetadata as any;
if (identifierOrServiceMetadata instanceof Array) {
identifierOrServiceMetadata.forEach((v: any) => this.set(v));
return this;
}
if (typeof identifierOrServiceMetadata === "string" || identifierOrServiceMetadata instanceof Token) {
return this.set({ id: identifierOrServiceMetadata, value });
newService = { id: identifierOrServiceMetadata, value };
}
if (
typeof identifierOrServiceMetadata === "object" &&
(identifierOrServiceMetadata as { service: Token<any> }).service
) {
return this.set({ id: (identifierOrServiceMetadata as { service: Token<any> }).service, value });
newService = { id: (identifierOrServiceMetadata as { service: Token<any> }).service, value };
}
if (identifierOrServiceMetadata instanceof Function) {
return this.set({ type: identifierOrServiceMetadata, id: identifierOrServiceMetadata, value });
newService = { type: identifierOrServiceMetadata, id: identifierOrServiceMetadata, value };
}
if (
typeof identifierOrServiceMetadata === "object" &&
(identifierOrServiceMetadata as { type: any }).type &&
!(identifierOrServiceMetadata as { id: any }).id
) {
const { type } = identifierOrServiceMetadata as any;
newService = { ...identifierOrServiceMetadata, id: type, value };
}

// const newService: ServiceMetadata<any, any> = arguments.length === 1 &&
// typeof identifierOrServiceMetadata === "object" &&
// !(identifierOrServiceMetadata instanceof Token) ? identifierOrServiceMetadata : undefined;
const newService: ServiceMetadata<any, any> = identifierOrServiceMetadata as any;
const service = this.findService(newService.id);
if (service && service.multiple !== true) {
Object.assign(service, newService);
if (newService.multiple === true) {
const otherServices = this.groupedServices.get(newService.id) || [];
this.groupedServices.set(newService.id, [...otherServices, newService]);
} else {
this.services.push(newService);
const service = this.findService(newService.id);
if (service && service.multiple !== true) {
Object.assign(service, newService);
} else {
this.services.set(newService.id, newService);
}
}

return this;
Expand All @@ -140,19 +160,15 @@ export class ContainerInstance {
* Removes services with a given service identifiers (tokens or types).
*/
remove (...ids: ServiceIdentifier[]): this {
ids.forEach((id) => {
this.filterServices(id).forEach((service) => {
this.services.splice(this.services.indexOf(service), 1);
});
});
ids.forEach((id) => this.services.delete(id));
return this;
}

/**
* Completely resets the container by removing all previously registered services from it.
*/
reset (): this {
this.services = [];
this.services.clear();
return this;
}

Expand All @@ -164,42 +180,15 @@ export class ContainerInstance {
* Filters registered service in the with a given service identifier.
*/
private filterServices (identifier: ServiceIdentifier): Array<ServiceMetadata<any, any>> {
return this.services.filter((service) => {
if (service.id) {
return service.id === identifier;
}

if (service.type && identifier instanceof Function) {
return service.type === identifier || identifier.prototype instanceof service.type;
}

return false;
});
return (this.services.get(identifier) as any) as Array<ServiceMetadata<any, any>>;
}

/**
* Finds registered service in the with a given service identifier.
*/
private findService (identifier: ServiceIdentifier): ServiceMetadata<any, any> | undefined {
return this.services.find((service) => {
if (service.id) {
if (
identifier instanceof Object &&
service.id instanceof Token &&
(identifier as any).service instanceof Token
) {
return service.id === (identifier as any).service;
}

return service.id === identifier;
}

if (service.type && identifier instanceof Function) {
return service.type === identifier;
} // todo: not sure why it was here || identifier.prototype instanceof service.type;

return false;
});
const id = (identifier as any).service instanceof Token ? (identifier as any).service : identifier;
return this.services.get(id);
}

/**
Expand Down Expand Up @@ -240,8 +229,8 @@ export class ContainerInstance {
throw new MissingProvidedServiceTypeError(identifier);
}

service = { type };
this.services.push(service);
service = { type, id: type };
this.services.set(service.id, service);
}

// setup constructor parameters for a newly initialized service
Expand Down Expand Up @@ -299,7 +288,7 @@ export class ContainerInstance {
*/
private initializeParams (type: Function, paramTypes: any[]): any[] {
return paramTypes.map((paramType, index) => {
const paramHandler = Container.handlers.find((handler) => handler.object === type && handler.index === index);
const paramHandler = (Container.handlers.get(type) || []).find((handler) => handler.index === index);
if (paramHandler) {
return paramHandler.value(this);
}
Expand All @@ -323,14 +312,7 @@ export class ContainerInstance {
* Applies all registered handlers on a given target class.
*/
private applyPropertyHandlers (target: Function, instance: { [key: string]: any }) {
Container.handlers.forEach((handler) => {
if (typeof handler.index === "number") {
return;
}
if (handler.object.constructor !== target && !(target.prototype instanceof handler.object.constructor)) {
return;
}

(Container.handlers.get(target) || []).forEach((handler) => {
instance[handler.propertyName] = handler.value(this);
});
}
Expand Down
16 changes: 8 additions & 8 deletions src/decorators/Service.ts
Expand Up @@ -14,7 +14,6 @@ export function Service<T, K extends keyof T> (
maybeFactory?: (...args: any[]) => any,
): any {
if (arguments.length === 2 || optionsOrServiceName instanceof Function) {
console.log("debug", { optionsOrServiceName, maybeFactory });
const serviceId = { service: new Token<T>() };
const dependencies = arguments.length === 2 ? (optionsOrServiceName as any[]) : [];
const factory = arguments.length === 2 ? maybeFactory : (optionsOrServiceName as (...args: any[]) => any);
Expand All @@ -31,21 +30,22 @@ export function Service<T, K extends keyof T> (
} else {
return (target: (...args: any[]) => any) => {
const service: ServiceMetadata<T, K> = {
id: target,
type: target,
};

if (typeof optionsOrServiceName === "string" || optionsOrServiceName instanceof Token) {
service.id = optionsOrServiceName;
service.id = optionsOrServiceName || target;
service.multiple = (optionsOrServiceName as ServiceOptions<T, K>).multiple;
service.global = (optionsOrServiceName as ServiceOptions<T, K>).global || false;
service.transient = (optionsOrServiceName as ServiceOptions<T, K>).transient;
} else if (optionsOrServiceName) {
// ServiceOptions
service.id = (optionsOrServiceName as ServiceOptions<T, K>).id;
service.factory = (optionsOrServiceName as ServiceOptions<T, K>).factory;
service.multiple = (optionsOrServiceName as ServiceOptions<T, K>).multiple;
service.global = (optionsOrServiceName as ServiceOptions<T, K>).global || false;
service.transient = (optionsOrServiceName as ServiceOptions<T, K>).transient;
const serviceOptions = optionsOrServiceName as ServiceOptions<T, K>;
service.id = serviceOptions.id || target;
service.factory = serviceOptions.factory;
service.multiple = serviceOptions.multiple;
service.global = serviceOptions.global || false;
service.transient = serviceOptions.transient;
}

Container.set(service);
Expand Down
3 changes: 2 additions & 1 deletion src/error/MissingProvidedServiceTypeError.ts
Expand Up @@ -5,7 +5,8 @@ export class MissingProvidedServiceTypeError extends Error {
name = "ServiceNotFoundError";

constructor (identifier: any) {
super(`Cannot determine a class of the requesting service "${identifier}"`);
super();
this.message = `Cannot determine a class of the requesting service "${JSON.stringify(identifier)}"`;
Object.setPrototypeOf(this, MissingProvidedServiceTypeError.prototype);
}
}
2 changes: 1 addition & 1 deletion src/types/ServiceMetadata.ts
Expand Up @@ -31,7 +31,7 @@ export interface ServiceMetadata<T, K extends keyof T> {
/**
* Service unique identifier.
*/
id?: ServiceIdentifier;
id: ServiceIdentifier;

/**
* Factory function used to initialize this service.
Expand Down
18 changes: 18 additions & 0 deletions test/Container.spec.ts
Expand Up @@ -6,6 +6,7 @@ import sinon_chai from "sinon-chai";

import { Container, Service, Token } from "../src";
import { ServiceNotFoundError } from "../src/error/ServiceNotFoundError";
import { MissingProvidedServiceTypeError } from "../src/error/MissingProvidedServiceTypeError";

chai.should();
chai.use(sinon_chai);
Expand Down Expand Up @@ -82,6 +83,23 @@ describe("Container", () => {
Container.get(SecondTestToken).name.should.be.equal("second");
});

it("should be able to set a service like { service: T }", () => {
class TestService {
constructor (public name: string) {}
}
const FirstTestToken = new Token<TestService>();

const firstService = new TestService("first");
Container.set({ service: FirstTestToken }, firstService);

const secondService = new TestService("second");
Container.set({ service: TestService }, secondService);

Container.get({ service: FirstTestToken }).name.should.be.equal("first");

expect(() => Container.get({ service: TestService })).to.throw(MissingProvidedServiceTypeError);
});

it("should override previous value if service is written second time", () => {
class TestService {
constructor (public name: string) {}
Expand Down
4 changes: 2 additions & 2 deletions test/github-issues/41/issue-41.spec.ts
Expand Up @@ -8,10 +8,10 @@ import { Container, Service, Token } from "../../../src";
chai.should();
chai.use(sinon_chai);

describe("github issues > #41 Token as service id in combination with factory", function () {
describe("github issues > #41 Token as service id in combination with factory", () => {
beforeEach(() => Container.reset());

it("should work properly", function () {
it("should work properly", () => {
interface SomeInterface {
foo (): string;
}
Expand Down
4 changes: 2 additions & 2 deletions test/github-issues/42/issue-42.spec.ts
Expand Up @@ -9,10 +9,10 @@ chai.should();
chai.use(sinon_chai);
const expect = chai.expect;

describe("github issues > #42 Exception not thrown on missing binding", function () {
describe("github issues > #42 Exception not thrown on missing binding", () => {
beforeEach(() => Container.reset());

it("should work properly", function () {
it("should work properly", () => {
interface Factory {
create (): void;
}
Expand Down

0 comments on commit 2d914c0

Please sign in to comment.