diff --git a/packages/injectable/core/index.d.ts b/packages/injectable/core/index.d.ts index 9e1607df..65acaf1f 100644 --- a/packages/injectable/core/index.d.ts +++ b/packages/injectable/core/index.d.ts @@ -195,6 +195,7 @@ export interface DiContainerForInjection { deregister(...injectables: Injectable[]): void; context: ContextItem[]; getInstances: GetInstances; + sourceNamespace: string | undefined; } export interface ILifecycle { diff --git a/packages/injectable/core/index.test-d.ts b/packages/injectable/core/index.test-d.ts index a31cca49..39bf1d39 100644 --- a/packages/injectable/core/index.test-d.ts +++ b/packages/injectable/core/index.test-d.ts @@ -482,3 +482,22 @@ di.unoverride(someInjectable); // given injectable, when unoverridden using injectionToken, typing is ok. di.unoverride(someStringInjectionToken); + +// given keyed singleton with sourceNamespace as key, typing is ok +const someKeyedSingletonWithSourceNamespaceAsKey = getInjectable({ + id: 'some-keyed-singleton-with-source-namespace-as-key', + + instantiate: di => { + expectType(di.sourceNamespace); + + return di.sourceNamespace; + }, + + lifecycle: lifecycleEnum.keyedSingleton({ + getInstanceKey: di => { + expectType(di.sourceNamespace); + + return di.sourceNamespace; + }, + }), +}); diff --git a/packages/injectable/core/src/dependency-injection-container/createContainer.js b/packages/injectable/core/src/dependency-injection-container/createContainer.js index fce6d0a6..8586045a 100644 --- a/packages/injectable/core/src/dependency-injection-container/createContainer.js +++ b/packages/injectable/core/src/dependency-injection-container/createContainer.js @@ -96,6 +96,7 @@ export default (containerId, { detectCycles = true } = {}) => { getDi: () => privateDi, checkForNoMatches, checkForSideEffects, + getNamespacedId, }); const decoratedPrivateInject = withInjectionDecorators( diff --git a/packages/injectable/core/src/dependency-injection-container/privateInjectFor.js b/packages/injectable/core/src/dependency-injection-container/privateInjectFor.js index 5e639f03..58545c26 100644 --- a/packages/injectable/core/src/dependency-injection-container/privateInjectFor.js +++ b/packages/injectable/core/src/dependency-injection-container/privateInjectFor.js @@ -11,8 +11,9 @@ export const privateInjectFor = getDi, checkForNoMatches, checkForSideEffects, + getNamespacedId, }) => - (alias, instantiationParameter, context = []) => { + (alias, instantiationParameter, context = [], source) => { const di = getDi(); const relatedInjectables = getRelatedInjectables(alias); @@ -36,6 +37,8 @@ export const privateInjectFor = instantiationParameter, context, instancesByInjectableMap, + source, + getNamespacedId, }); }; @@ -45,6 +48,8 @@ const getInstance = ({ instantiationParameter, context: oldContext, instancesByInjectableMap, + source, + getNamespacedId, }) => { const newContext = [ ...oldContext, @@ -97,6 +102,12 @@ const getInstance = ({ source: injectableToBeInstantiated, }); }, + + get sourceNamespace() { + return ( + getNamespacedId(source).split(':').slice(0, -1).join(':') || undefined + ); + }, }; const instanceKey = injectableToBeInstantiated.lifecycle.getInstanceKey( diff --git a/packages/injectable/core/src/scenarios/access-to-namespace.test.js b/packages/injectable/core/src/scenarios/access-to-namespace.test.js new file mode 100644 index 00000000..7f78d8a8 --- /dev/null +++ b/packages/injectable/core/src/scenarios/access-to-namespace.test.js @@ -0,0 +1,173 @@ +import getInjectable from '../getInjectable/getInjectable'; +import createContainer from '../dependency-injection-container/createContainer'; +import lifecycleEnum from '../dependency-injection-container/lifecycleEnum'; +import { getInjectionToken } from '@ogre-tools/injectable'; + +describe('access-to-namespace', () => { + it('given keyed singleton using source namespace as the key, when injected from different scopes, injected instances are scope-specific', () => { + const di = createContainer('irrelevant'); + + const someSourceNamespaceSpecificInjectionToken = getInjectionToken({ + id: 'some-injection-token', + }); + + const someSourceNamespaceSpecificInjectable = getInjectable({ + id: 'some-source-namespace-specific', + instantiate: di => message => + di.sourceNamespace ? `${di.sourceNamespace}/${message}` : message, + + lifecycle: lifecycleEnum.keyedSingleton({ + getInstanceKey: di => di.sourceNamespace, + }), + + injectionToken: someSourceNamespaceSpecificInjectionToken, + }); + + const registerInjectableInScope1Injectable = getInjectable({ + id: 'scope-1', + instantiate: di => injectable => di.register(injectable), + scope: true, + }); + + const someFunctionalityInScope1Injectable = getInjectable({ + id: 'some-functionality-in-scope-1', + instantiate: di => di.inject(someSourceNamespaceSpecificInjectionToken), + }); + + const registerInjectableInScope2Injectable = getInjectable({ + id: 'scope-2', + instantiate: di => injectable => di.register(injectable), + scope: true, + }); + + const someFunctionalityInScope2Injectable = getInjectable({ + id: 'register-injectable-in-scope-2', + instantiate: di => di.inject(someSourceNamespaceSpecificInjectionToken), + }); + + const someFunctionalityInRootScopeInjectable = getInjectable({ + id: 'some-functionality-in-root-scope', + instantiate: di => di.inject(someSourceNamespaceSpecificInjectionToken), + }); + + di.register( + registerInjectableInScope1Injectable, + registerInjectableInScope2Injectable, + ); + + const registerInjectableInScope1 = di.inject( + registerInjectableInScope1Injectable, + ); + + const registerInjectableInScope2 = di.inject( + registerInjectableInScope2Injectable, + ); + + di.register(someSourceNamespaceSpecificInjectable); + registerInjectableInScope1(someFunctionalityInScope1Injectable); + registerInjectableInScope2(someFunctionalityInScope2Injectable); + di.register(someFunctionalityInRootScopeInjectable); + + expect([ + di.inject(someFunctionalityInScope1Injectable)('some-value'), + di.inject(someFunctionalityInScope2Injectable)('some-other-value'), + di.inject(someFunctionalityInRootScopeInjectable)( + 'some-value-without-namespace', + ), + ]).toEqual([ + 'scope-1/some-value', + 'scope-2/some-other-value', + 'some-value-without-namespace', + ]); + }); + + it('given keyed singleton using source namespace as the key, when injected from nested scopes, injected instances are scope-specific', () => { + const di = createContainer('irrelevant'); + + const someSourceNamespaceSpecificInjectionToken = getInjectionToken({ + id: 'some-injection-token', + }); + + const someSourceNamespaceSpecificInjectable = getInjectable({ + id: 'some-source-namespace-specific', + + instantiate: di => message => `${di.sourceNamespace}/${message}`, + + lifecycle: lifecycleEnum.keyedSingleton({ + getInstanceKey: di => di.sourceNamespace, + }), + + injectionToken: someSourceNamespaceSpecificInjectionToken, + }); + + const registerInjectableInScope1Injectable = getInjectable({ + id: 'scope-1', + instantiate: di => injectable => di.register(injectable), + scope: true, + }); + + const registerInjectableInNestedScope1Injectable = getInjectable({ + id: 'nested-scope-1', + instantiate: di => injectable => di.register(injectable), + scope: true, + }); + + const someFunctionalityInNestedScope1Injectable = getInjectable({ + id: 'some-functionality-in-scope-1', + instantiate: di => di.inject(someSourceNamespaceSpecificInjectionToken), + }); + + const registerInjectableInScope2Injectable = getInjectable({ + id: 'scope-2', + instantiate: di => injectable => di.register(injectable), + scope: true, + }); + + const registerInjectableInNestedScope2Injectable = getInjectable({ + id: 'nested-scope-2', + instantiate: di => injectable => di.register(injectable), + scope: true, + }); + + const someFunctionalityInNestedScope2Injectable = getInjectable({ + id: 'register-injectable-in-scope-2', + instantiate: di => di.inject(someSourceNamespaceSpecificInjectionToken), + }); + + di.register( + registerInjectableInScope1Injectable, + registerInjectableInScope2Injectable, + ); + + const registerInjectableInScope1 = di.inject( + registerInjectableInScope1Injectable, + ); + + const registerInjectableInScope2 = di.inject( + registerInjectableInScope2Injectable, + ); + + registerInjectableInScope1(registerInjectableInNestedScope1Injectable); + registerInjectableInScope2(registerInjectableInNestedScope2Injectable); + + const registerInjectableInNestedScope1 = di.inject( + registerInjectableInNestedScope1Injectable, + ); + + const registerInjectableInNestedScope2 = di.inject( + registerInjectableInNestedScope2Injectable, + ); + + di.register(someSourceNamespaceSpecificInjectable); + registerInjectableInNestedScope1(someFunctionalityInNestedScope1Injectable); + registerInjectableInNestedScope2(someFunctionalityInNestedScope2Injectable); + + expect([ + di.inject(someFunctionalityInNestedScope1Injectable)('some-value'), + di.inject(someFunctionalityInNestedScope2Injectable)('some-other-value'), + ]).toEqual([ + 'scope-1:nested-scope-1/some-value', + 'scope-2:nested-scope-2/some-other-value', + ]); + }); +});