Skip to content

Commit

Permalink
feat: allow to overwrite service inside a inner context (for test mocks)
Browse files Browse the repository at this point in the history
  • Loading branch information
lbguilherme committed Jan 21, 2021
1 parent 2bbf36e commit 85fa7d7
Show file tree
Hide file tree
Showing 5 changed files with 190 additions and 17 deletions.
95 changes: 95 additions & 0 deletions spec/inject.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-shadow */
import { popInjectionContext, pushInjectionContext, registerValue, registerScopedValue, registerService, setupScope, use } from "../src";

describe("env", () => {
Expand Down Expand Up @@ -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();
}
});
});
70 changes: 59 additions & 11 deletions src/global-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,36 +5,84 @@ export class GlobalContext {

private readonly services = new Map<string, Service>();

private readonly singletons = new Map<string, unknown>();
private readonly singletonInstances = new Map<string, unknown>();

private readonly values = new Map<string, unknown>();

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 {
Expand Down
20 changes: 19 additions & 1 deletion src/scope-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
16 changes: 12 additions & 4 deletions src/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ function createServiceInstance(service: Service) {
}

export function registerService<T extends ServiceType>(lifetime: ServiceLifetime, type: T, ...ctorArgs: ConstructorParameters<T>) {
if (getGlobalContext().hasService(type.name)) {
if (getGlobalContext().hasServiceSkipParent(type.name)) {
throw new Error(`Service '${type.name}' is already registered`);
}

Expand Down Expand Up @@ -74,13 +74,21 @@ export function useService<T extends ServiceType>(type: T): InstanceType<T> {
}

case "singleton": {
if (getGlobalContext().hasSingleton(type.name)) {
return getGlobalContext().getSingleton(type.name) as InstanceType<T>;
if (getGlobalContext().hasSingletonInstanceSkipParent(type.name)) {
return getGlobalContext().getSingletonInstance(type.name) as InstanceType<T>;
}

if (getGlobalContext().hasSingletonInstance(type.name)) {
const instance = getGlobalContext().getSingletonInstance(type.name);

if (instance instanceof service.type) {
return instance as InstanceType<T>;
}
}

const newInstance = createServiceInstance(service) as InstanceType<T>;

getGlobalContext().setSingleton(type.name, newInstance);
getGlobalContext().setSingletonInstance(type.name, newInstance);

return newInstance;
}
Expand Down
6 changes: 5 additions & 1 deletion src/value.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export function useValue<NameT extends keyof UseTypeMapInternal>(name: NameT): U
}

export function registerValue<NameT extends keyof UseTypeMapInternal>(name: NameT, value: UseTypeMapInternal[NameT]) {
if (getGlobalContext().hasValue(name)) {
if (getGlobalContext().hasValueSkipParent(name)) {
throw new Error(`Value '${name}' is already registered`);
}

Expand All @@ -33,5 +33,9 @@ export function registerScopedValue<NameT extends keyof UseTypeMapInternal>(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);
}

0 comments on commit 85fa7d7

Please sign in to comment.