diff --git a/spec/inject.test.ts b/spec/inject.test.ts index 3618f95..2a7cf61 100644 --- a/spec/inject.test.ts +++ b/spec/inject.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-shadow */ import { popInjectionContext, pushInjectionContext, registerValue, registerScopedValue, registerService, setupScope, use } from "../src"; describe("env", () => { @@ -316,4 +317,98 @@ describe("env", () => { expect(() => use(A)).toThrowError("Cyclic service dependency on constructor: 'A' -> 'B' -> 'C' -> 'A'"); }); + + it("allows a service to be registered again inside a inner injection context", () => { + class A { + id = 1; + } + + registerService("transient", A); + + expect(use(A).id).toBe(1); + + pushInjectionContext(); + try { + registerService( + "transient", + class A { + id = 2; + }, + ); + + expect(use(A).id).toBe(2); + } finally { + popInjectionContext(); + } + + expect(use(A).id).toBe(1); + }); + + it("disallows a service to be registered again inside a inner injection context if it has already been used", () => { + class A { + id = 1; + } + + registerService("transient", A); + + expect(use(A).id).toBe(1); + + pushInjectionContext(); + try { + expect(use(A).id).toBe(1); + + expect(() => + registerService( + "transient", + class A { + id = 2; + }, + ), + ).toThrowError("Service 'A' is already registered"); + + expect(use(A).id).toBe(1); + } finally { + popInjectionContext(); + } + + expect(use(A).id).toBe(1); + }); + + it("only create new singleton instance if needed, in case the service it registered again in a inner context", () => { + class A { + id = 1; + } + + registerService("singleton", A); + + const instance = use(A); + + expect(instance.id).toBe(1); + + pushInjectionContext(); + try { + registerService( + "singleton", + class A { + id = 2; + }, + ); + + const innerInstance = use(A); + + expect(innerInstance.id).toBe(2); + } finally { + popInjectionContext(); + } + + pushInjectionContext(); + try { + const innerInstance = use(A); + + expect(innerInstance.id).toBe(1); + expect(innerInstance).toBe(instance); + } finally { + popInjectionContext(); + } + }); }); diff --git a/src/global-context.ts b/src/global-context.ts index 2647a11..43c690d 100644 --- a/src/global-context.ts +++ b/src/global-context.ts @@ -5,36 +5,84 @@ export class GlobalContext { private readonly services = new Map(); - private readonly singletons = new Map(); + private readonly singletonInstances = new Map(); private readonly values = new Map(); getService(name: string): Service | undefined { - return this.services.get(name) ?? this.parent?.getService(name); + let service = this.services.get(name); + + if (service) { + return service; + } + + service = this.parent?.getService(name); + + if (service) { + this.services.set(name, service); + } + + return service; } - hasService(name: string): boolean { - return this.services.has(name) || (this.parent?.hasService(name) ?? false); + hasServiceSkipParent(name: string): boolean { + return this.services.has(name); } setService(name: string, value: Service) { this.services.set(name, value); } - getSingleton(name: string): unknown { - return this.singletons.has(name) ? this.singletons.get(name) : this.parent?.getSingleton(name); + getSingletonInstance(name: string): unknown { + if (this.singletonInstances.has(name)) { + return this.singletonInstances.get(name); + } + + if (this.parent?.hasSingletonInstance(name)) { + const singletonInstance = this.parent.getSingletonInstance(name); + + if (singletonInstance) { + this.singletonInstances.set(name, singletonInstance); + } + + return singletonInstance; + } + + return undefined; + } + + hasSingletonInstanceSkipParent(name: string): boolean { + return this.singletonInstances.has(name); } - hasSingleton(name: string): boolean { - return this.singletons.has(name) || (this.parent?.hasSingleton(name) ?? false); + hasSingletonInstance(name: string): boolean { + return this.singletonInstances.has(name) || (this.parent?.hasSingletonInstance(name) ?? false); } - setSingleton(name: string, value: unknown) { - this.singletons.set(name, value); + setSingletonInstance(name: string, value: unknown) { + this.singletonInstances.set(name, value); } getValue(name: string): unknown { - return this.values.has(name) ? this.values.get(name) : this.parent?.getValue(name); + if (this.values.has(name)) { + return this.values.get(name); + } + + if (this.parent?.hasValue(name)) { + const value = this.parent.getValue(name); + + if (value) { + this.values.set(name, value); + } + + return value; + } + + return undefined; + } + + hasValueSkipParent(name: string): boolean { + return this.values.has(name); } hasValue(name: string): boolean { diff --git a/src/scope-context.ts b/src/scope-context.ts index b443804..1b95a7f 100644 --- a/src/scope-context.ts +++ b/src/scope-context.ts @@ -20,7 +20,25 @@ export class ScopeContext { } getValue(name: string): unknown { - return this.values.has(name) ? this.values.get(name) : this.parent?.getValue(name); + if (this.values.has(name)) { + return this.values.get(name); + } + + if (this.parent?.hasValue(name)) { + const value = this.parent.getValue(name); + + if (value) { + this.values.set(name, value); + } + + return value; + } + + return undefined; + } + + hasValueSkipParent(name: string): boolean { + return this.values.has(name); } hasValue(name: string): boolean { diff --git a/src/service.ts b/src/service.ts index 2e3ca38..c320669 100644 --- a/src/service.ts +++ b/src/service.ts @@ -37,7 +37,7 @@ function createServiceInstance(service: Service) { } export function registerService(lifetime: ServiceLifetime, type: T, ...ctorArgs: ConstructorParameters) { - if (getGlobalContext().hasService(type.name)) { + if (getGlobalContext().hasServiceSkipParent(type.name)) { throw new Error(`Service '${type.name}' is already registered`); } @@ -74,13 +74,21 @@ export function useService(type: T): InstanceType { } case "singleton": { - if (getGlobalContext().hasSingleton(type.name)) { - return getGlobalContext().getSingleton(type.name) as InstanceType; + if (getGlobalContext().hasSingletonInstanceSkipParent(type.name)) { + return getGlobalContext().getSingletonInstance(type.name) as InstanceType; + } + + if (getGlobalContext().hasSingletonInstance(type.name)) { + const instance = getGlobalContext().getSingletonInstance(type.name); + + if (instance instanceof service.type) { + return instance as InstanceType; + } } const newInstance = createServiceInstance(service) as InstanceType; - getGlobalContext().setSingleton(type.name, newInstance); + getGlobalContext().setSingletonInstance(type.name, newInstance); return newInstance; } diff --git a/src/value.ts b/src/value.ts index f6a6634..730440d 100644 --- a/src/value.ts +++ b/src/value.ts @@ -19,7 +19,7 @@ export function useValue(name: NameT): U } export function registerValue(name: NameT, value: UseTypeMapInternal[NameT]) { - if (getGlobalContext().hasValue(name)) { + if (getGlobalContext().hasValueSkipParent(name)) { throw new Error(`Value '${name}' is already registered`); } @@ -33,5 +33,9 @@ export function registerScopedValue(name throw new Error(`Scoped value '${name}' can't be registered outside a scope`); } + if (scopeContext.hasValueSkipParent(name)) { + throw new Error(`Value '${name}' is already registered`); + } + scopeContext.setValue(name, value); }