Skip to content

Commit

Permalink
feat: Introduce specificity of injection tokens
Browse files Browse the repository at this point in the history
Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>
(cherry picked from commit 992551d)
  • Loading branch information
Iku-turso committed Apr 16, 2024
1 parent 421d10c commit c5fe446
Show file tree
Hide file tree
Showing 33 changed files with 1,273 additions and 68 deletions.
80 changes: 65 additions & 15 deletions packages/injectable/core/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,36 @@ export type Instantiate<InjectionInstance, InstantiationParam> = {
): InjectionInstance;
};

export interface InjectionToken<InjectionInstance, InstantiationParam> {
export interface InjectionToken<
InjectionInstance,
InstantiationParam = void,
SpecificInjectionTokenFactory extends (
...args: any[]
) => SpecificInjectionToken<InjectionInstance, InstantiationParam> = (
id: string,
) => SpecificInjectionToken<InjectionInstance, InstantiationParam>,
> {
template: InjectionInstance;
instantiationParameter: InstantiationParam;
key: Symbol;
id: string;
for: SpecificInjectionTokenFactory;
}

export interface SpecificInjectionToken<
InjectionInstance,
InstantiationParam = void,
SpecificInjectionTokenFactory extends (
...args: any[]
) => SpecificInjectionToken<InjectionInstance, InstantiationParam> = (
id: string,
) => SpecificInjectionToken<InjectionInstance, InstantiationParam>,
> extends InjectionToken<
InjectionInstance,
InstantiationParam,
SpecificInjectionTokenFactory
> {
speciality: any;
}

export interface Injectable<
Expand Down Expand Up @@ -109,14 +134,41 @@ export function getInjectableBunch<Type>(bunch: Type): InjectableBunch<Type>;
export function getInjectionToken<
InjectionInstance,
InstantiationParam = void,
SpecificInjectionTokenFactory extends (
...args: any[]
) => SpecificInjectionToken<InjectionInstance, InstantiationParam> = (
id: string,
) => SpecificInjectionToken<InjectionInstance, InstantiationParam>,
>(options: {
id: string;
}): InjectionToken<InjectionInstance, InstantiationParam>;
specificInjectionTokenFactory?: SpecificInjectionTokenFactory;
}): InjectionToken<
InjectionInstance,
InstantiationParam,
SpecificInjectionTokenFactory
>;

export function getSpecificInjectionToken<
InjectionInstance,
InstantiationParam = void,
SpecificInjectionTokenFactory extends (
...args: any[]
) => SpecificInjectionToken<InjectionInstance, InstantiationParam> = (
id: string,
) => SpecificInjectionToken<InjectionInstance, InstantiationParam>,
>(options: {
id: string;
speciality: any;
}): SpecificInjectionToken<
InjectionInstance,
InstantiationParam,
SpecificInjectionTokenFactory
>;

export type InjectWithoutParameter = <InjectionInstance>(
key:
| Injectable<InjectionInstance, unknown>
| InjectionToken<InjectionInstance, void>,
| InjectionToken<InjectionInstance>,
) => InjectionInstance;

export type InjectWithParameter = <InjectionInstance, InstantiationParam>(
Expand All @@ -137,13 +189,13 @@ export type InjectFactory = <InjectionInstance, InstantiationParam extends {}>(
export type GetInstances = <InjectionInstance>(
alias:
| Injectable<InjectionInstance, unknown>
| InjectionToken<InjectionInstance, void>,
| InjectionToken<InjectionInstance>,
) => InjectionInstance[];

export type SpecificInjectWithoutParameter<InjectionInstance> = (
key:
| Injectable<InjectionInstance, unknown>
| InjectionToken<InjectionInstance, void>,
| InjectionToken<InjectionInstance>,
) => InjectionInstance;

export type SpecificInjectWithParameter<InjectionInstance, InstantiationParam> =
Expand All @@ -162,8 +214,8 @@ export type SpecificInject<InjectionInstance, InstantiationParam> =
interface InjectMany {
<InjectionInstance>(
key:
| Injectable<InjectionInstance, unknown, void>
| InjectionToken<InjectionInstance, void>,
| Injectable<InjectionInstance, unknown>
| InjectionToken<InjectionInstance>,
): InjectionInstance[];

<InjectionInstance, InstantiationParam>(
Expand All @@ -186,8 +238,8 @@ export type InjectionInstanceWithMeta<InjectionInstance> = {
interface InjectManyWithMeta {
<InjectionInstance>(
key:
| Injectable<InjectionInstance, unknown, void>
| InjectionToken<InjectionInstance, void>,
| Injectable<InjectionInstance, unknown>
| InjectionToken<InjectionInstance>,
): InjectionInstanceWithMeta<InjectionInstance>[];

<InjectionInstance, InstantiationParam>(
Expand All @@ -201,8 +253,8 @@ interface InjectManyWithMeta {
interface InjectWithMeta {
<InjectionInstance>(
key:
| Injectable<InjectionInstance, unknown, void>
| InjectionToken<InjectionInstance, void>,
| Injectable<InjectionInstance, unknown>
| InjectionToken<InjectionInstance>,
): InjectionInstanceWithMeta<InjectionInstance>;

<InjectionInstance, InstantiationParam>(
Expand Down Expand Up @@ -312,8 +364,7 @@ export const createInjectionTargetDecorator: CreateInjectionTargetDecorator;
* This kind of decorator does not respect the lifecycle of the injectables but instead is called on every call to `di.inject`
*/
export const injectionDecoratorToken: InjectionToken<
InjectionTargetDecorator<any, any, any>,
void
InjectionTargetDecorator<any, any, any>
>;

export type SpecificInstantiationTargetDecorator<
Expand Down Expand Up @@ -377,8 +428,7 @@ export const createInstantiationTargetDecorator: CreateInstantiationTargetDecora
* This kind of decorator respects the lifecycle of the injectables.
*/
export const instantiationDecoratorToken: InjectionToken<
InstantiationTargetDecorator<any, any, any>,
void
InstantiationTargetDecorator<any, any, any>
>;

export const registrationCallbackToken: RegistrationCallback;
Expand Down
7 changes: 6 additions & 1 deletion packages/injectable/core/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import getInjectable from './src/getInjectable/getInjectable';
import isInjectable from './src/getInjectable/isInjectable';
import lifecycleEnum from './src/dependency-injection-container/lifecycleEnum';

import getInjectionToken from './src/getInjectionToken/getInjectionToken';
import {
getInjectionToken,
getSpecificInjectionToken,
} from './src/getInjectionToken/getInjectionToken';

import isInjectionToken from './src/getInjectionToken/isInjectionToken';

import getInjectableBunch from './src/getInjectableBunch/getInjectableBunch';
Expand All @@ -31,6 +35,7 @@ export {
getInjectableBunch,
isInjectableBunch,
getInjectionToken,
getSpecificInjectionToken,
isInjectionToken,
lifecycleEnum,
toFlatInjectables,
Expand Down
114 changes: 113 additions & 1 deletion packages/injectable/core/index.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { expectError, expectNotType, expectType } from 'tsd';
import { expectError, expectAssignable, expectNotType, expectType } from 'tsd';

import {
createContainer,
Expand All @@ -22,6 +22,8 @@ import {
getKeyedSingletonCompositeKey,
isInjectableBunch,
InjectableBunch,
SpecificInjectionToken,
getSpecificInjectionToken,
} from '.';

const di = createContainer('some-container');
Expand Down Expand Up @@ -559,3 +561,113 @@ expectType<boolean>(di.hasRegistrations(someInjectable));

// given token, typing for "alias has registrations" is ok
expectType<boolean>(di.hasRegistrations(someInjectionToken));

// given general injection token without generics, and a more specific token created by it, typing is ok
const someGeneralInjectionTokenWithoutGenerics = getInjectionToken<number>({
id: 'some-general-token-without-generics',
});

expectAssignable<{
id: string;
for: (id: string) => SpecificInjectionToken<number>;
}>(someGeneralInjectionTokenWithoutGenerics);

const someSpecificInjectionTokenWithoutGenerics =
someGeneralInjectionTokenWithoutGenerics.for('some-specific-token');

expectAssignable<{
id: string;
for: (id: string) => SpecificInjectionToken<number>;
}>(someSpecificInjectionTokenWithoutGenerics);

// given general injection token with generics, and a more specific token created by it, typing is ok
const someGeneralInjectionTokenWithGenerics = getInjectionToken<
{ someProperty: unknown },
void,
<Speciality>(
speciality: Speciality,
) => SpecificInjectionToken<{ someProperty: Speciality }>
>({
id: 'some-general-token',

specificInjectionTokenFactory: <Speciality>(speciality: Speciality) =>
getSpecificInjectionToken<{ someProperty: Speciality }>({
id: 'some-specific-token',
speciality,
}),
});

expectType<
InjectionToken<
{ someProperty: unknown },
void,
<Speciality>(
speciality: Speciality,
) => SpecificInjectionToken<{ someProperty: Speciality }>
>
>(someGeneralInjectionTokenWithGenerics);

const someSpecificInjectionToken = someGeneralInjectionTokenWithGenerics.for(
'some-specific-token-as-string',
);

expectType<SpecificInjectionToken<{ someProperty: string }>>(
someSpecificInjectionToken,
);

const someMoreSpecificInjectionToken = someSpecificInjectionToken.for(
'some-more-specific-token-as-string',
);

expectType<SpecificInjectionToken<{ someProperty: string }>>(
someMoreSpecificInjectionToken,
);

expectType<{ someProperty: string }>(
di.inject(someGeneralInjectionTokenWithGenerics.for('some-string')),
);

expectType<{ someProperty: number }>(
di.inject(someGeneralInjectionTokenWithGenerics.for(42)),
);

expectType<{ someProperty: number }>(
di.inject(
someGeneralInjectionTokenWithGenerics.for(42).for('some-deeper-speciality'),
),
);

// given general injection token with generics and instantiation parameter, and a more specific token created by it, typing is ok
const someGeneralInjectionTokenWithGenericsAndParameter = getInjectionToken<
{ someProperty: unknown },
{ someInstantiationParameter: unknown },
<Speciality>(
speciality: Speciality,
) => SpecificInjectionToken<
{ someProperty: Speciality },
{ someInstantiationParameter: Speciality }
>
>({
id: 'some-general-token',

specificInjectionTokenFactory: <Speciality>(speciality: Speciality) =>
getSpecificInjectionToken<
{ someProperty: Speciality },
{ someInstantiationParameter: Speciality }
>({
id: 'some-specific-token',
speciality,
}),
});

expectType<{ someProperty: number }>(
di.inject(someGeneralInjectionTokenWithGenericsAndParameter.for(42), {
someInstantiationParameter: 37,
}),
);

expectType<{ someProperty: number }[]>(
di.injectMany(someGeneralInjectionTokenWithGenericsAndParameter.for(42), {
someInstantiationParameter: 37,
}),
);
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { deregistrationCallbackToken } from './tokens';
import toFlatInjectables from './toFlatInjectables';
import isInjectionToken from '../getInjectionToken/isInjectionToken';
import { getRelatedTokens } from './getRelatedTokens';

export const deregisterFor =
({
Expand Down Expand Up @@ -95,11 +96,11 @@ export const deregisterSingleFor =
injectableSet.delete(injectable);
namespacedIdByInjectableMap.delete(injectable);

if (injectable.injectionToken) {
injectablesByInjectionToken
.get(injectable.injectionToken)
.delete(injectable);
}
const tokens = getRelatedTokens(injectable.injectionToken);

tokens.forEach(token => {
injectablesByInjectionToken.get(token).delete(injectable);
});

overridingInjectables.delete(injectable);
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import isInjectable from '../getInjectable/isInjectable';

export const getRelatedInjectablesFor =
({ injectablesByInjectionToken, injectableSet }) =>
alias =>
isInjectable(alias)
? injectableSet.has(alias)
? [alias]
: []
: [...(injectablesByInjectionToken.get(alias)?.values() || [])];
alias => {
if (isInjectable(alias)) {
return injectableSet.has(alias) ? [alias] : [];
} else {
return [...(injectablesByInjectionToken.get(alias)?.values() || [])];
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const getRelatedTokens = token =>
token === undefined
? []
: [token, ...getRelatedTokens(token.specificTokenOf)];
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { getNamespacedIdFor } from './getNamespacedIdFor';
import { registrationCallbackToken } from './tokens';
import toFlatInjectables from './toFlatInjectables';
import { DeepMap } from '@lensapp/fp';
import { getRelatedTokens } from './getRelatedTokens';

export const registerFor =
({ registerSingle, injectMany }) =>
Expand Down Expand Up @@ -65,14 +66,14 @@ export const registerSingleFor =
namespacedIdByInjectableMap.set(injectable, namespacedId);
instancesByInjectableMap.set(injectable, new DeepMap());

if (injectable.injectionToken) {
const token = injectable.injectionToken;
const tokens = getRelatedTokens(injectable.injectionToken);

tokens.forEach(token => {
const injectablesSet =
injectablesByInjectionToken.get(token) || new Set();

injectablesSet.add(injectable);

injectablesByInjectionToken.set(token, injectablesSet);
}
});
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import getInjectionToken from '../getInjectionToken/getInjectionToken';
import { getInjectionToken } from '../getInjectionToken/getInjectionToken';

export const registrationCallbackToken = getInjectionToken({
id: 'registration-callback-token',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import isInjectable from './isInjectable';
import getInjectable from './getInjectable';
import getInjectionToken from '../getInjectionToken/getInjectionToken';
import { getInjectionToken } from '../getInjectionToken/getInjectionToken';

describe('isInjectable', () => {
it('an injectable is an injectable', () => {
Expand Down
Loading

0 comments on commit c5fe446

Please sign in to comment.