Skip to content

Commit

Permalink
create child injector
Browse files Browse the repository at this point in the history
  • Loading branch information
Emanuel Hein committed Sep 1, 2022
1 parent 975812c commit 21bce84
Show file tree
Hide file tree
Showing 9 changed files with 503 additions and 83 deletions.
21 changes: 21 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Test",
"request": "launch",
"runtimeArgs": [
"run-script",
"test"
],
"runtimeExecutable": "npm",
"skipFiles": [
"<node_internals>/**"
],
"type": "node"
},
]
}
109 changes: 109 additions & 0 deletions src/child-injector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { Initiator } from './private-base';
import { InjectFactory, InjectToken } from './public-base';
import { TypeInjector } from './type-injector';

export class ChildInjector extends TypeInjector {
static withIdent(ident: symbol) {
return {
from(parent: TypeInjector): TypeInjector {
return new ChildInjector(ident, parent);
}
};
}

private _ownInstances = [] as any[];

private constructor(
public readonly ident: symbol,
private _parent: TypeInjector,
) {
super();
}

provideValue<T>(token: InjectToken<T>, value: T): TypeInjector {
this._ownInstances.push(value);
return super.provideValue(token, value);
}

private _createInOwnScope<T>(token: InjectToken<T>, initiator: Initiator, factory: InjectFactory<T>): InstanceWithSource<T> {
this._markAsInCreation(token, initiator);
const args = factory.deps.map((dep) => this._get(dep, token));
const created = factory.create(...args);
this._ownInstances.push(created);
this._instances.set(token, created);
this._finishedCreation(token);
return {
instance: created,
fromParentScope: false,
};
}

/**
* Do not create an own instance but ask parent scope for an instance.
*
* After checking that no own instance is needed this method can get called to
* query the parent scope to resolve the token. This might create a new instance
* in the (parent) parent if it doesn't exist yet.
*
* This instance is linked into _instances to prevent further calls with the same
* token to repeat all dependency checks.
*
* @param token
* @returns instance from parent + flag that it is from parent
*/
private _useInstanceFromParentScope<T>(token: InjectToken<T>): InstanceWithSource<T> {
const refFromParent = this._parent.get(token);
this._instances.set(token, refFromParent);
return {
instance: refFromParent,
fromParentScope: true,
}
}

/**
* Checks if there are own/overridden dependencies.
*
* Therefore this function will query for all dependencies which
* might trigger lazy creation. But all of them are cached in the
* appropriate scope and needed for the creation anyway so there's
* not much wasted computing time (only a duplicate lookup).
*
* Even if it does not create the requested value it's important
* to add it to the values in creation to detect dependency cycles.
*
* @param token
* @param initiator
*/
private _hasOwnDependencies(token: InjectToken<unknown>, initiator: Initiator, factory: InjectFactory<unknown>): boolean {
this._markAsInCreation(token, initiator);
const hasOwnDependencies = factory.deps.some((dep) => {
const instance = this._get(dep, token);
return this._ownInstances.includes(instance);
});
this._finishedCreation(token);
return hasOwnDependencies;
}

private _createWithSource<T>(token: InjectToken<T>, initiator: Initiator): InstanceWithSource<T> {
const providedFactory = this._factories.get(token);
if (providedFactory) {
return this._createInOwnScope<T>(token, initiator, providedFactory);
}

const parentFactory = this._parent.getFactory<T>(token);
if (this._hasOwnDependencies(token, initiator, parentFactory)) {
return this._createInOwnScope(token, initiator, parentFactory);
} else {
return this._useInstanceFromParentScope(token);
}
}

protected _create<T>(token: InjectToken<T>, initiator: Initiator): T {
return this._createWithSource(token, initiator).instance;
}
}

interface InstanceWithSource<T> {
instance: T,
fromParentScope: boolean,
}
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export * from './child-injector';
export * from './public-base';
export * from './logger';
export * from './type-injector';
9 changes: 9 additions & 0 deletions src/private-base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { InjectableClass, InjectToken } from './public-base';

export type Initiator = InjectToken<unknown> | typeof startOfCycle;

export function hasInjectConfig<T>(token: unknown): token is InjectableClass<T> {
return !!(token as Partial<InjectableClass<T>>).injectConfig;
}

export const startOfCycle = 'startOfCycle' as const;
37 changes: 37 additions & 0 deletions src/public-base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* Every class can get an InjectableClass by adding a static injectConfig property.
*
* For classes that can get instantiated without constructor arguments it
* is *not* required to add an injectConfig. An injectConfig is required to
* tell the TypedInjector to use a constructor with arguments to create a
* class instance.
*
* @see {@link InjectConfig InjectConfig}
*/
export type InjectableClass<T> = (new (..._args: any[]) => T) & {
injectConfig: InjectConfig;
}

export interface InjectConfig {
/**
* Inject tokens for all arguments required to create an injectable value.
*
* - For classes the dependencies have to match the consturctor parameters
* - For all other functions (like factories) the tokens have to match the parameters
*
* In both cases "match" means that the inject tokens return the right types of
* all parameters in the same order as they are needed for the function/constructor call.
*
* The dependencies of an inject token are not created before the inject token
* itself gets created.
*/
deps: InjectToken<unknown>[];
}

export interface InjectFactory<T> extends InjectConfig {
create: (...args: any[]) => T,
}

export type ConstructorWithoutArguments<T> = new () => T;

export type InjectToken<T> = ConstructorWithoutArguments<T> | InjectableClass<T> | symbol;
3 changes: 1 addition & 2 deletions src/type-injector-0-basics.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { expect } from 'chai';
import { Logger } from './logger';
import { InjectConfig, TypeInjector } from './type-injector';
import { InjectConfig, Logger, TypeInjector } from './index';

describe('type injector basics', () => {
it('should be able to instantiate an injector', () => {
Expand Down
2 changes: 1 addition & 1 deletion src/type-injector-1-inject-token.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect } from 'chai';
import { InjectConfig, TypeInjector } from './type-injector';
import { InjectConfig, TypeInjector } from './index';

describe('inject tokens', () => {
it('should be possible to use any constructor without arguments as inject token', () => {
Expand Down
Loading

0 comments on commit 21bce84

Please sign in to comment.