typed-inject
Advanced tools
Comparing version 4.0.0 to 5.0.0
@@ -0,1 +1,17 @@ | ||
# [5.0.0](https://github.com/nicojs/typed-inject/compare/v4.0.0...v5.0.0) (2024-12-13) | ||
### Features | ||
* **child injector:** Add `createChildInjector` ([#72](https://github.com/nicojs/typed-inject/issues/72)) ([e103564](https://github.com/nicojs/typed-inject/commit/e10356495f27428db85c6a074f369364cfad4871)) | ||
* improve error messages ([#66](https://github.com/nicojs/typed-inject/issues/66)) ([64f7640](https://github.com/nicojs/typed-inject/commit/64f7640a68b76b3d4cac979110798333339309e4)) | ||
* **node:** drop support for node 16 ([#71](https://github.com/nicojs/typed-inject/issues/71)) ([028cd45](https://github.com/nicojs/typed-inject/commit/028cd4553383521e9b7761bba327d545faabf4cc)) | ||
### BREAKING CHANGES | ||
* **node:** Please use Node 18 or higher | ||
# [4.0.0](https://github.com/nicojs/typed-inject/compare/v3.0.1...v4.0.0) (2023-05-05) | ||
@@ -2,0 +18,0 @@ |
@@ -6,10 +6,62 @@ import { InjectableClass, InjectableFunction } from './Injectable.js'; | ||
export interface Injector<TContext = {}> { | ||
/** | ||
* This method creates a new instance of class `injectable` by populating its constructor arguments from the injector and returns it. | ||
* @param Class The class to instantiate. | ||
*/ | ||
injectClass<R, Tokens extends readonly InjectionToken<TContext>[]>(Class: InjectableClass<TContext, R, Tokens>): R; | ||
injectFunction<R, Tokens extends readonly InjectionToken<TContext>[]>(Class: InjectableFunction<TContext, R, Tokens>): R; | ||
/** | ||
* This method injects the function with requested tokens from the injector, invokes it and returns the result. | ||
* @param fn The function to inject. | ||
*/ | ||
injectFunction<R, Tokens extends readonly InjectionToken<TContext>[]>(fn: InjectableFunction<TContext, R, Tokens>): R; | ||
/** | ||
* Resolve tokens by hand. | ||
* @param token The token to resolve. | ||
*/ | ||
resolve<Token extends keyof TContext>(token: Token): TContext[Token]; | ||
/** | ||
* Create a child injector that can provide a value using `value` for token `'token'`. The new child injector can resolve all tokens the parent injector can, as well as the new `'token'`. | ||
* @param token The token to associate with the value. | ||
* @param value The value to provide. | ||
*/ | ||
provideValue<Token extends string, R>(token: Token, value: R): Injector<TChildContext<TContext, R, Token>>; | ||
/** | ||
* Create a child injector that can provide a value using instances of `Class` for token `'token'`. The new child injector can resolve all tokens the parent injector can, as well as the new `'token'`. | ||
* @param token The token to associate with the value. | ||
* @param Class The class to instantiate to provide the value. | ||
* @param scope Decide whether the value must be cached after the factory is invoked once. Use `Scope.Singleton` to enable caching (default), or `Scope.Transient` to disable caching. | ||
*/ | ||
provideClass<Token extends string, R, Tokens extends readonly InjectionToken<TContext>[]>(token: Token, Class: InjectableClass<TContext, R, Tokens>, scope?: Scope): Injector<TChildContext<TContext, R, Token>>; | ||
/** | ||
* Create a child injector that can provide a value using `factory` for token `'token'`. The new child injector can resolve all tokens the parent injector can and the new `'token'`. | ||
* @param token The token to associate with the value. | ||
* @param factory A function that creates a value using instances of the tokens in `'Tokens'`. | ||
* @param scope Decide whether the value must be cached after the factory is invoked once. Use `Scope.Singleton` to enable caching (default), or `Scope.Transient` to disable caching. | ||
*/ | ||
provideFactory<Token extends string, R, Tokens extends readonly InjectionToken<TContext>[]>(token: Token, factory: InjectableFunction<TContext, R, Tokens>, scope?: Scope): Injector<TChildContext<TContext, R, Token>>; | ||
/** | ||
* Create a child injector that can provide exactly the same as the parent injector. | ||
* Contrary to its `provideXxx` counterparts,this will create a new disposable scope without providing additional injectable values. | ||
* @example | ||
* ```ts | ||
* const parentInjector = createInjector().provideValue('foo', 'bar'); | ||
* for(const task of tasks) { | ||
* try { | ||
* const scope = parentInjector.createChildInjector(); | ||
* const foo = scope.provideClass('baz', DisposableBaz).injectClass(Foo); | ||
* foo.handle(task); | ||
* } finally { | ||
* await scope.dispose(); // Dispose the scope, including instances of DisposableBaz | ||
* // Next task gets a fresh scope | ||
* } | ||
* } | ||
* ``` | ||
**/ | ||
createChildInjector(): Injector<TContext>; | ||
/** | ||
* Explicitly dispose the `injector`. | ||
* @see {@link https://github.com/nicojs/typed-inject?tab=readme-ov-file#disposing-provided-stuff} | ||
*/ | ||
dispose(): Promise<void>; | ||
} | ||
//# sourceMappingURL=Injector.d.ts.map |
@@ -1,6 +0,7 @@ | ||
export type TChildContext<TParentContext, TProvided, CurrentToken extends string> = { | ||
[K in keyof (TParentContext & { | ||
[K in CurrentToken]: TProvided; | ||
})]: K extends CurrentToken ? TProvided : K extends keyof TParentContext ? TParentContext[K] : never; | ||
export type Simplify<T> = {} & { | ||
[K in keyof T]: T[K]; | ||
}; | ||
export type TChildContext<TParentContext, TProvided, CurrentToken extends string> = Simplify<{ | ||
[K in keyof TParentContext | CurrentToken]: K extends CurrentToken ? TProvided : K extends keyof TParentContext ? TParentContext[K] : never; | ||
}>; | ||
//# sourceMappingURL=TChildContext.d.ts.map |
@@ -9,3 +9,2 @@ import type { InjectionTarget } from './api/InjectionTarget.js'; | ||
readonly path: InjectionTarget[]; | ||
readonly cause: Error; | ||
constructor(path: InjectionTarget[], cause: Error); | ||
@@ -12,0 +11,0 @@ static create(target: InjectionTarget, error: Error): InjectionError; |
@@ -43,6 +43,6 @@ /* | ||
export class InjectionError extends TypedInjectError { | ||
path; | ||
constructor(path, cause) { | ||
super(`Could not ${describeInjectAction(path[0])} ${path.map(name).join(' -> ')}. Cause: ${cause.message}`); | ||
super(`Could not ${describeInjectAction(path[0])} ${path.map(name).join(' -> ')}. Cause: ${cause.message}`, { cause }); | ||
this.path = path; | ||
this.cause = cause; | ||
} | ||
@@ -49,0 +49,0 @@ static create(target, error) { |
@@ -5,3 +5,3 @@ /* eslint-disable @typescript-eslint/no-unsafe-argument */ | ||
/* eslint-disable @typescript-eslint/no-unsafe-return */ | ||
import { INJECTOR_TOKEN, TARGET_TOKEN } from './api/InjectionToken.js'; | ||
import { INJECTOR_TOKEN, TARGET_TOKEN, } from './api/InjectionToken.js'; | ||
import { Scope } from './api/Scope.js'; | ||
@@ -27,2 +27,7 @@ import { InjectionError, InjectorDisposedError } from './errors.js'; | ||
┃ | ||
┏━━━━━━━━━━━━━┻━━━━━━━━━━━━━┓ | ||
┃ ChildWithProvidedInjector ┃ | ||
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ | ||
▲ | ||
┃ | ||
┏━━━━━━━━━━━━━━━━━┻━┳━━━━━━━━━━━━━━━━┓ | ||
@@ -34,6 +39,3 @@ ┏━━━━━━━━┻━━━━━━━━┓ ┏━━━━━━━━┻━━━━━━┓ ┏━━━━━━━┻━━━━━━━┓ | ||
class AbstractInjector { | ||
constructor() { | ||
this.childInjectors = new Set(); | ||
this.isDisposed = false; | ||
} | ||
childInjectors = new Set(); | ||
injectClass(Class, providedIn) { | ||
@@ -102,2 +104,6 @@ this.throwIfDisposed(Class); | ||
} | ||
isDisposed = false; | ||
createChildInjector() { | ||
return new ChildInjector(this); | ||
} | ||
async dispose() { | ||
@@ -117,2 +123,3 @@ if (!this.isDisposed) { | ||
resolveInternal(token) { | ||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions | ||
throw new Error(`No provider found for "${token}"!.`); | ||
@@ -125,8 +132,10 @@ } | ||
class ChildInjector extends AbstractInjector { | ||
constructor(parent, token, scope) { | ||
parent; | ||
async disposeInjectedValues() { } | ||
resolveInternal(token, target) { | ||
return this.parent.resolve(token, target); | ||
} | ||
constructor(parent) { | ||
super(); | ||
this.parent = parent; | ||
this.token = token; | ||
this.scope = scope; | ||
this.disposables = new Set(); | ||
} | ||
@@ -137,5 +146,12 @@ async dispose() { | ||
} | ||
async disposeInjectedValues() { | ||
const promisesToAwait = [...this.disposables.values()].map((disposable) => disposable.dispose()); | ||
await Promise.all(promisesToAwait); | ||
} | ||
class ChildWithProvidedInjector extends ChildInjector { | ||
token; | ||
scope; | ||
cached; | ||
disposables = new Set(); | ||
constructor(parent, token, scope) { | ||
super(parent); | ||
this.token = token; | ||
this.scope = scope; | ||
} | ||
@@ -173,4 +189,9 @@ resolveInternal(token, target) { | ||
} | ||
async disposeInjectedValues() { | ||
const promisesToAwait = [...this.disposables.values()].map((disposable) => disposable.dispose()); | ||
await Promise.all(promisesToAwait); | ||
} | ||
} | ||
class ValueProvider extends ChildInjector { | ||
class ValueProvider extends ChildWithProvidedInjector { | ||
value; | ||
constructor(parent, token, value) { | ||
@@ -184,3 +205,4 @@ super(parent, token, Scope.Transient); | ||
} | ||
class FactoryProvider extends ChildInjector { | ||
class FactoryProvider extends ChildWithProvidedInjector { | ||
injectable; | ||
constructor(parent, token, scope, injectable) { | ||
@@ -194,3 +216,4 @@ super(parent, token, scope); | ||
} | ||
class ClassProvider extends ChildInjector { | ||
class ClassProvider extends ChildWithProvidedInjector { | ||
injectable; | ||
constructor(parent, token, scope, injectable) { | ||
@@ -197,0 +220,0 @@ super(parent, token, scope); |
export function isDisposable(maybeDisposable) { | ||
const asDisposable = maybeDisposable; | ||
return asDisposable && asDisposable.dispose && typeof asDisposable.dispose === 'function'; | ||
return (asDisposable && | ||
asDisposable.dispose && | ||
typeof asDisposable.dispose === 'function'); | ||
} | ||
//# sourceMappingURL=utils.js.map |
{ | ||
"name": "typed-inject", | ||
"version": "4.0.0", | ||
"version": "5.0.0", | ||
"description": "Type safe dependency injection framework for TypeScript", | ||
@@ -11,3 +11,3 @@ "main": "dist/src/index.js", | ||
"clean": "rimraf dist", | ||
"lint": "eslint . --ext .js,.ts --ignore-path .gitignore --ignore-pattern testResources/**/*.ts --ignore-pattern stryker.conf.js", | ||
"lint": "eslint", | ||
"build": "tsc -b", | ||
@@ -32,3 +32,3 @@ "test": "c8 --exclude-after-remap=false \"--exclude=dist/test/**/*.js\" --check-coverage --reporter=html --report-dir=reports/coverage --lines 100 --functions 100 --branches 100 npm run test:all", | ||
"engines": { | ||
"node": ">=16" | ||
"node": ">=18" | ||
}, | ||
@@ -53,26 +53,27 @@ "keywords": [ | ||
"devDependencies": { | ||
"@stryker-mutator/core": "^6.4.2", | ||
"@stryker-mutator/mocha-runner": "^6.4.2", | ||
"@stryker-mutator/typescript-checker": "^6.4.2", | ||
"@types/chai": "^4.3.5", | ||
"@types/mocha": "^10.0.1", | ||
"@types/node": "^20.1.0", | ||
"@types/sinon": "^10.0.14", | ||
"@types/sinon-chai": "^3.2.9", | ||
"@typescript-eslint/eslint-plugin": "^5.59.2", | ||
"@typescript-eslint/parser": "^5.59.2", | ||
"c8": "^7.13.0", | ||
"chai": "^4.3.7", | ||
"conventional-changelog-cli": "^2.2.2", | ||
"eslint": "^8.40.0", | ||
"eslint-config-prettier": "^8.8.0", | ||
"eslint-plugin-prettier": "^4.2.1", | ||
"mocha": "^10.2.0", | ||
"prettier": "^2.8.8", | ||
"rimraf": "^5.0.0", | ||
"sinon": "^15.0.4", | ||
"sinon-chai": "^3.7.0", | ||
"@eslint/js": "^9.16.0", | ||
"@stryker-mutator/core": "^8.7.1", | ||
"@stryker-mutator/mocha-runner": "^8.7.1", | ||
"@stryker-mutator/typescript-checker": "^8.7.1", | ||
"@types/chai": "^5.0.1", | ||
"@types/mocha": "^10.0.10", | ||
"@types/node": "^22.10.2", | ||
"@types/sinon": "^17.0.3", | ||
"@types/sinon-chai": "^4.0.0", | ||
"@typescript-eslint/eslint-plugin": "^8.18.0", | ||
"@typescript-eslint/parser": "^8.18.0", | ||
"c8": "^10.1.3", | ||
"chai": "^5.1.2", | ||
"conventional-changelog-cli": "^5.0.0", | ||
"eslint-plugin-chai-friendly": "^1.0.1", | ||
"eslint-plugin-prettier": "^5.2.1", | ||
"mocha": "^11.0.1", | ||
"prettier": "^3.4.2", | ||
"rimraf": "^6.0.1", | ||
"sinon": "^19.0.2", | ||
"sinon-chai": "^4.0.0", | ||
"source-map-support": "^0.5.21", | ||
"typescript": "~5.0.4" | ||
"typescript": "~5.7.2", | ||
"typescript-eslint": "^8.18.0" | ||
} | ||
} |
110
README.md
@@ -17,16 +17,15 @@ [![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fnicojs%2Ftyped-inject%2Fmaster)](https://dashboard.stryker-mutator.io/reports/github.com/nicojs/typed-inject/master) | ||
- [🗺️ Installation](#installation) | ||
- [🎁 Usage](#usage) | ||
- [💭 Motivation](#motivation) | ||
- [🗝️ Typesafe? How?](#typesafe-how) | ||
- [👶 Child injectors](#child-injectors) | ||
- [🎄 Decorate your dependencies](#decorate-your-dependencies) | ||
- [♻ Lifecycle control](#lifecycle-control) | ||
- [🚮 Disposing provided stuff](#disposing-provided-stuff) | ||
- [✨ Magic tokens](#magic-tokens) | ||
- [😬 Error handling](#error-handling) | ||
- [📖 API reference](#api-reference) | ||
- [🤝 Commendation](#commendation) | ||
* [🗺️ Installation](#installation) | ||
* [🎁 Usage](#usage) | ||
* [💭 Motivation](#motivation) | ||
* [🗝️ Typesafe? How?](#typesafe-how) | ||
* [👶 Child injectors](#child-injectors) | ||
* [🎄 Decorate your dependencies](#decorate-your-dependencies) | ||
* [♻ Lifecycle control](#lifecycle-control) | ||
* [🚮 Disposing provided stuff](#disposing-provided-stuff) | ||
* [✨ Magic tokens](#magic-tokens) | ||
* [😬 Error handling](#error-handling) | ||
* [📖 API reference](#api-reference) | ||
* [🤝 Commendation](#commendation) | ||
<a name="installation"></a> | ||
@@ -70,3 +69,3 @@ | ||
console.log(message); | ||
} | ||
}, | ||
}; | ||
@@ -80,7 +79,12 @@ | ||
class MyService { | ||
constructor(private http: HttpClient, private log: Logger) {} | ||
constructor( | ||
private http: HttpClient, | ||
private log: Logger, | ||
) {} | ||
public static inject = ['httpClient', 'logger'] as const; | ||
} | ||
const appInjector = createInjector().provideValue('logger', logger).provideClass('httpClient', HttpClient); | ||
const appInjector = createInjector() | ||
.provideValue('logger', logger) | ||
.provideClass('httpClient', HttpClient); | ||
@@ -111,3 +115,6 @@ const myService = appInjector.injectClass(MyService); | ||
class MyService { | ||
constructor(private http: HttpClient, private log: Logger) {} | ||
constructor( | ||
private http: HttpClient, | ||
private log: Logger, | ||
) {} | ||
public static inject = ['logger', 'httpClient'] as const; | ||
@@ -117,3 +124,5 @@ // ERROR! Types of parameters 'http' and 'args_0' are incompatible | ||
const appInjector = createInjector().provideValue('logger', logger).provideClass('httpClient', HttpClient); | ||
const appInjector = createInjector() | ||
.provideValue('logger', logger) | ||
.provideClass('httpClient', HttpClient); | ||
@@ -213,3 +222,3 @@ const myService = appInjector.injectClass(MyService); | ||
console.log('after call'); | ||
} | ||
}, | ||
}; | ||
@@ -219,3 +228,5 @@ } | ||
const fooProvider = createInjector().provideClass('foo', Foo).provideFactory('foo', fooDecorator); | ||
const fooProvider = createInjector() | ||
.provideClass('foo', Foo) | ||
.provideFactory('foo', fooDecorator); | ||
const foo = fooProvider.resolve('foo'); | ||
@@ -250,3 +261,5 @@ | ||
const fooProvider = injector.provideFactory('log', loggerFactory, Scope.Transient).provideClass('foo', Foo, Scope.Singleton); | ||
const fooProvider = injector | ||
.provideFactory('log', loggerFactory, Scope.Transient) | ||
.provideClass('foo', Foo, Scope.Singleton); | ||
const foo = fooProvider.resolve('foo'); | ||
@@ -360,9 +373,9 @@ const fooCopy = fooProvider.resolve('foo'); | ||
static inject = ['foo', 'bar'] as const; | ||
constructor(public foo: Foo, public bar: Bar) {} | ||
constructor( | ||
public foo: Foo, | ||
public bar: Bar, | ||
) {} | ||
} | ||
const rootInjector = createInjector(); | ||
rootInjector | ||
.provideClass('foo', Foo) | ||
.provideClass('bar', Bar) | ||
.injectClass(Baz); | ||
rootInjector.provideClass('foo', Foo).provideClass('bar', Bar).injectClass(Baz); | ||
await fooProvider.dispose(); | ||
@@ -381,5 +394,5 @@ // => "Foo disposed" | ||
| Token name | Token value | Description | | ||
| ---------------- | ------------- | -------------------------------------------------------------------------------------------------- | | ||
| `INJECTOR_TOKEN` | `'$injector'` | Injects the current injector | | ||
| Token name | Token value | Description | | ||
| ---------------- | ------------- | --------------------------------------------------------------------------------------------------- | | ||
| `INJECTOR_TOKEN` | `'$injector'` | Injects the current injector | | ||
| `TARGET_TOKEN` | `'$target'` | The class or function in which the current values are injected, or `undefined` if resolved directly | | ||
@@ -390,3 +403,8 @@ | ||
```ts | ||
import { createInjector, Injector, TARGET_TOKEN, INJECTOR_TOKEN } from 'typed-inject'; | ||
import { | ||
createInjector, | ||
Injector, | ||
TARGET_TOKEN, | ||
INJECTOR_TOKEN, | ||
} from 'typed-inject'; | ||
@@ -490,3 +508,3 @@ class Foo { | ||
This method injects the function with requested tokens from the injector, invokes it and returns the result. | ||
This method injects the function with requested tokens from the injector, invokes it and returns the result. | ||
@@ -545,6 +563,10 @@ It is a shortcut for calling the provided function with the values from the injector. | ||
loggerFactory.inject = [TARGET_TOKEN] as const; | ||
const fooBarInjector = fooInjector.provideFactory('logger', loggerFactory, Scope.Transient); | ||
const fooBarInjector = fooInjector.provideFactory( | ||
'logger', | ||
loggerFactory, | ||
Scope.Transient, | ||
); | ||
``` | ||
#### `injector.provideFactory(token: Token, Class: InjectableClass<TContext>, scope = Scope.Singleton): Injector<ChildContext<TContext, Token, R>>` | ||
#### `injector.provideClass(token: Token, Class: InjectableClass<TContext>, scope = Scope.Singleton): Injector<ChildContext<TContext, Token, R>>` | ||
@@ -555,2 +577,20 @@ Create a child injector that can provide a value using instances of `Class` for token `'token'`. The new child injector can resolve all tokens the parent injector can, as well as the new `'token'`. | ||
#### `injector.createChildInjector(): Injector<TContext>` | ||
Create a child injector that can provide exactly the same as the parent injector. Contrary to its `provideXxx` counterparts,this will create a new disposable scope without providing additional injectable values. | ||
```ts | ||
const parentInjector = createInjector().provideValue('foo', 'bar'); | ||
for (const task of tasks) { | ||
try { | ||
const scope = parentInjector.createChildInjector(); | ||
const foo = scope.provideClass('baz', DisposableBaz).injectClass(Foo); | ||
foo.handle(task); | ||
} finally { | ||
await scope.dispose(); // Dispose the scope, including instances of DisposableBaz | ||
// Next task gets a fresh scope | ||
} | ||
} | ||
``` | ||
#### `injector.dispose(): Promise<void>` | ||
@@ -561,4 +601,4 @@ | ||
1. Call `dispose` on each child injector created from this injector. | ||
2. It will call `dispose` on any dependency created by the injector (if it exists) using `provideClass` or `provideFactory` (**not** `provideValue` or `injectXXX`). | ||
3. It will also await any promise that might have been returned by disposable dependencies. | ||
2. It will call `dispose` on any dependency created by the injector (if it exists) using `provideClass` or `provideFactory` (**not** `provideValue` or `injectXXX`). | ||
3. It will also await any promise that might have been returned by disposable dependencies. | ||
@@ -565,0 +605,0 @@ _Note: this behavior changed since v2. Before v2, the parent injector was always disposed before the child injector._ |
@@ -1,14 +0,26 @@ | ||
import { InjectionToken, InjectorToken, TargetToken } from './InjectionToken.js'; | ||
import { | ||
InjectionToken, | ||
InjectorToken, | ||
TargetToken, | ||
} from './InjectionToken.js'; | ||
import { Injector } from './Injector.js'; | ||
export type CorrespondingType<TContext, T extends InjectionToken<TContext>> = T extends InjectorToken | ||
export type CorrespondingType< | ||
TContext, | ||
T extends InjectionToken<TContext>, | ||
> = T extends InjectorToken | ||
? Injector<TContext> | ||
: T extends TargetToken | ||
? Function | undefined | ||
: T extends keyof TContext | ||
? TContext[T] | ||
: never; | ||
? Function | undefined | ||
: T extends keyof TContext | ||
? TContext[T] | ||
: never; | ||
export type CorrespondingTypes<TContext, TS extends readonly InjectionToken<TContext>[]> = { | ||
[K in keyof TS]: TS[K] extends InjectionToken<TContext> ? CorrespondingType<TContext, TS[K]> : never; | ||
export type CorrespondingTypes< | ||
TContext, | ||
TS extends readonly InjectionToken<TContext>[], | ||
> = { | ||
[K in keyof TS]: TS[K] extends InjectionToken<TContext> | ||
? CorrespondingType<TContext, TS[K]> | ||
: never; | ||
}; |
import { CorrespondingTypes } from './CorrespondingType.js'; | ||
import { InjectionToken } from './InjectionToken.js'; | ||
export type InjectableClass<TContext, R, Tokens extends readonly InjectionToken<TContext>[]> = | ||
| ClassWithInjections<TContext, R, Tokens> | ||
| ClassWithoutInjections<R>; | ||
export type InjectableClass< | ||
TContext, | ||
R, | ||
Tokens extends readonly InjectionToken<TContext>[], | ||
> = ClassWithInjections<TContext, R, Tokens> | ClassWithoutInjections<R>; | ||
export interface ClassWithInjections<TContext, R, Tokens extends readonly InjectionToken<TContext>[]> { | ||
export interface ClassWithInjections< | ||
TContext, | ||
R, | ||
Tokens extends readonly InjectionToken<TContext>[], | ||
> { | ||
new (...args: CorrespondingTypes<TContext, Tokens>): R; | ||
@@ -15,7 +21,15 @@ readonly inject: Tokens; | ||
export type InjectableFunction<TContext, R, Tokens extends readonly InjectionToken<TContext>[]> = | ||
export type InjectableFunction< | ||
TContext, | ||
R, | ||
Tokens extends readonly InjectionToken<TContext>[], | ||
> = | ||
| InjectableFunctionWithInject<TContext, R, Tokens> | ||
| InjectableFunctionWithoutInject<R>; | ||
export interface InjectableFunctionWithInject<TContext, R, Tokens extends readonly InjectionToken<TContext>[]> { | ||
export interface InjectableFunctionWithInject< | ||
TContext, | ||
R, | ||
Tokens extends readonly InjectionToken<TContext>[], | ||
> { | ||
(...args: CorrespondingTypes<TContext, Tokens>): R; | ||
@@ -27,4 +41,8 @@ readonly inject: Tokens; | ||
export type Injectable<TContext, R, Tokens extends readonly InjectionToken<TContext>[]> = | ||
export type Injectable< | ||
TContext, | ||
R, | ||
Tokens extends readonly InjectionToken<TContext>[], | ||
> = | ||
| InjectableClass<TContext, R, Tokens> | ||
| InjectableFunction<TContext, R, Tokens>; |
@@ -5,2 +5,5 @@ export type InjectorToken = '$injector'; | ||
export const TARGET_TOKEN: TargetToken = '$target'; | ||
export type InjectionToken<TContext> = InjectorToken | TargetToken | keyof TContext; | ||
export type InjectionToken<TContext> = | ||
| InjectorToken | ||
| TargetToken | ||
| keyof TContext; |
@@ -7,17 +7,86 @@ import { InjectableClass, InjectableFunction } from './Injectable.js'; | ||
export interface Injector<TContext = {}> { | ||
injectClass<R, Tokens extends readonly InjectionToken<TContext>[]>(Class: InjectableClass<TContext, R, Tokens>): R; | ||
injectFunction<R, Tokens extends readonly InjectionToken<TContext>[]>(Class: InjectableFunction<TContext, R, Tokens>): R; | ||
/** | ||
* This method creates a new instance of class `injectable` by populating its constructor arguments from the injector and returns it. | ||
* @param Class The class to instantiate. | ||
*/ | ||
injectClass<R, Tokens extends readonly InjectionToken<TContext>[]>( | ||
Class: InjectableClass<TContext, R, Tokens>, | ||
): R; | ||
/** | ||
* This method injects the function with requested tokens from the injector, invokes it and returns the result. | ||
* @param fn The function to inject. | ||
*/ | ||
injectFunction<R, Tokens extends readonly InjectionToken<TContext>[]>( | ||
fn: InjectableFunction<TContext, R, Tokens>, | ||
): R; | ||
/** | ||
* Resolve tokens by hand. | ||
* @param token The token to resolve. | ||
*/ | ||
resolve<Token extends keyof TContext>(token: Token): TContext[Token]; | ||
provideValue<Token extends string, R>(token: Token, value: R): Injector<TChildContext<TContext, R, Token>>; | ||
provideClass<Token extends string, R, Tokens extends readonly InjectionToken<TContext>[]>( | ||
/** | ||
* Create a child injector that can provide a value using `value` for token `'token'`. The new child injector can resolve all tokens the parent injector can, as well as the new `'token'`. | ||
* @param token The token to associate with the value. | ||
* @param value The value to provide. | ||
*/ | ||
provideValue<Token extends string, R>( | ||
token: Token, | ||
value: R, | ||
): Injector<TChildContext<TContext, R, Token>>; | ||
/** | ||
* Create a child injector that can provide a value using instances of `Class` for token `'token'`. The new child injector can resolve all tokens the parent injector can, as well as the new `'token'`. | ||
* @param token The token to associate with the value. | ||
* @param Class The class to instantiate to provide the value. | ||
* @param scope Decide whether the value must be cached after the factory is invoked once. Use `Scope.Singleton` to enable caching (default), or `Scope.Transient` to disable caching. | ||
*/ | ||
provideClass< | ||
Token extends string, | ||
R, | ||
Tokens extends readonly InjectionToken<TContext>[], | ||
>( | ||
token: Token, | ||
Class: InjectableClass<TContext, R, Tokens>, | ||
scope?: Scope | ||
scope?: Scope, | ||
): Injector<TChildContext<TContext, R, Token>>; | ||
provideFactory<Token extends string, R, Tokens extends readonly InjectionToken<TContext>[]>( | ||
/** | ||
* Create a child injector that can provide a value using `factory` for token `'token'`. The new child injector can resolve all tokens the parent injector can and the new `'token'`. | ||
* @param token The token to associate with the value. | ||
* @param factory A function that creates a value using instances of the tokens in `'Tokens'`. | ||
* @param scope Decide whether the value must be cached after the factory is invoked once. Use `Scope.Singleton` to enable caching (default), or `Scope.Transient` to disable caching. | ||
*/ | ||
provideFactory< | ||
Token extends string, | ||
R, | ||
Tokens extends readonly InjectionToken<TContext>[], | ||
>( | ||
token: Token, | ||
factory: InjectableFunction<TContext, R, Tokens>, | ||
scope?: Scope | ||
scope?: Scope, | ||
): Injector<TChildContext<TContext, R, Token>>; | ||
/** | ||
* Create a child injector that can provide exactly the same as the parent injector. | ||
* Contrary to its `provideXxx` counterparts,this will create a new disposable scope without providing additional injectable values. | ||
* @example | ||
* ```ts | ||
* const parentInjector = createInjector().provideValue('foo', 'bar'); | ||
* for(const task of tasks) { | ||
* try { | ||
* const scope = parentInjector.createChildInjector(); | ||
* const foo = scope.provideClass('baz', DisposableBaz).injectClass(Foo); | ||
* foo.handle(task); | ||
* } finally { | ||
* await scope.dispose(); // Dispose the scope, including instances of DisposableBaz | ||
* // Next task gets a fresh scope | ||
* } | ||
* } | ||
* ``` | ||
**/ | ||
createChildInjector(): Injector<TContext>; | ||
/** | ||
* Explicitly dispose the `injector`. | ||
* @see {@link https://github.com/nicojs/typed-inject?tab=readme-ov-file#disposing-provided-stuff} | ||
*/ | ||
dispose(): Promise<void>; | ||
} |
@@ -1,7 +0,14 @@ | ||
export type TChildContext<TParentContext, TProvided, CurrentToken extends string> = { | ||
[K in keyof (TParentContext & { [K in CurrentToken]: TProvided })]: K extends CurrentToken | ||
// Forces typescript to explicitly calculate the complicated type | ||
export type Simplify<T> = {} & { [K in keyof T]: T[K] }; | ||
export type TChildContext< | ||
TParentContext, | ||
TProvided, | ||
CurrentToken extends string, | ||
> = Simplify<{ | ||
[K in keyof TParentContext | CurrentToken]: K extends CurrentToken // | ||
? TProvided | ||
: K extends keyof TParentContext | ||
? TParentContext[K] | ||
: never; | ||
}; | ||
? TParentContext[K] | ||
: never; | ||
}>; |
@@ -41,3 +41,5 @@ import type { InjectionTarget } from './api/InjectionTarget.js'; | ||
constructor(target: InjectionTarget) { | ||
super(`Injector is already disposed. Please don't use it anymore. Tried to ${describeInjectAction(target)} ${name(target)}.`); | ||
super( | ||
`Injector is already disposed. Please don't use it anymore. Tried to ${describeInjectAction(target)} ${name(target)}.`, | ||
); | ||
} | ||
@@ -47,4 +49,10 @@ } | ||
export class InjectionError extends TypedInjectError { | ||
constructor(public readonly path: InjectionTarget[], public readonly cause: Error) { | ||
super(`Could not ${describeInjectAction(path[0])} ${path.map(name).join(' -> ')}. Cause: ${cause.message}`); | ||
constructor( | ||
public readonly path: InjectionTarget[], | ||
cause: Error, | ||
) { | ||
super( | ||
`Could not ${describeInjectAction(path[0])} ${path.map(name).join(' -> ')}. Cause: ${cause.message}`, | ||
{ cause }, | ||
); | ||
} | ||
@@ -54,3 +62,3 @@ | ||
if (error instanceof InjectionError) { | ||
return new InjectionError([target, ...error.path], error.cause); | ||
return new InjectionError([target, ...error.path], error.cause as Error); | ||
} else { | ||
@@ -57,0 +65,0 @@ return new InjectionError([target], error); |
@@ -5,4 +5,12 @@ /* eslint-disable @typescript-eslint/no-unsafe-argument */ | ||
/* eslint-disable @typescript-eslint/no-unsafe-return */ | ||
import { type InjectionToken, INJECTOR_TOKEN, TARGET_TOKEN } from './api/InjectionToken.js'; | ||
import type { InjectableClass, InjectableFunction, Injectable } from './api/Injectable.js'; | ||
import { | ||
type InjectionToken, | ||
INJECTOR_TOKEN, | ||
TARGET_TOKEN, | ||
} from './api/InjectionToken.js'; | ||
import type { | ||
InjectableClass, | ||
InjectableFunction, | ||
Injectable, | ||
} from './api/Injectable.js'; | ||
import type { Injector } from './api/Injector.js'; | ||
@@ -34,2 +42,7 @@ import type { Disposable } from './api/Disposable.js'; | ||
┃ | ||
┏━━━━━━━━━━━━━┻━━━━━━━━━━━━━┓ | ||
┃ ChildWithProvidedInjector ┃ | ||
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ | ||
▲ | ||
┃ | ||
┏━━━━━━━━━━━━━━━━━┻━┳━━━━━━━━━━━━━━━━┓ | ||
@@ -44,3 +57,6 @@ ┏━━━━━━━━┻━━━━━━━━┓ ┏━━━━━━━━┻━━━━━━┓ ┏━━━━━━━┻━━━━━━━┓ | ||
public injectClass<R, Tokens extends InjectionToken<TContext>[]>(Class: InjectableClass<TContext, R, Tokens>, providedIn?: Function): R { | ||
public injectClass<R, Tokens extends InjectionToken<TContext>[]>( | ||
Class: InjectableClass<TContext, R, Tokens>, | ||
providedIn?: Function, | ||
): R { | ||
this.throwIfDisposed(Class); | ||
@@ -55,3 +71,6 @@ try { | ||
public injectFunction<R, Tokens extends InjectionToken<TContext>[]>(fn: InjectableFunction<TContext, R, Tokens>, providedIn?: Function): R { | ||
public injectFunction<R, Tokens extends InjectionToken<TContext>[]>( | ||
fn: InjectableFunction<TContext, R, Tokens>, | ||
providedIn?: Function, | ||
): R { | ||
this.throwIfDisposed(fn); | ||
@@ -68,3 +87,3 @@ try { | ||
injectable: Injectable<TContext, any, Tokens>, | ||
target?: Function | ||
target?: Function, | ||
): any[] { | ||
@@ -84,3 +103,6 @@ const tokens: InjectionToken<TContext>[] = (injectable as any).inject || []; | ||
public provideValue<Token extends string, R>(token: Token, value: R): AbstractInjector<TChildContext<TContext, R, Token>> { | ||
public provideValue<Token extends string, R>( | ||
token: Token, | ||
value: R, | ||
): AbstractInjector<TChildContext<TContext, R, Token>> { | ||
this.throwIfDisposed(token); | ||
@@ -92,6 +114,10 @@ const provider = new ValueProvider(this, token, value); | ||
public provideClass<Token extends string, R, Tokens extends InjectionToken<TContext>[]>( | ||
public provideClass< | ||
Token extends string, | ||
R, | ||
Tokens extends InjectionToken<TContext>[], | ||
>( | ||
token: Token, | ||
Class: InjectableClass<TContext, R, Tokens>, | ||
scope = DEFAULT_SCOPE | ||
scope = DEFAULT_SCOPE, | ||
): AbstractInjector<TChildContext<TContext, R, Token>> { | ||
@@ -103,6 +129,10 @@ this.throwIfDisposed(token); | ||
} | ||
public provideFactory<Token extends string, R, Tokens extends InjectionToken<TContext>[]>( | ||
public provideFactory< | ||
Token extends string, | ||
R, | ||
Tokens extends InjectionToken<TContext>[], | ||
>( | ||
token: Token, | ||
factory: InjectableFunction<TContext, R, Tokens>, | ||
scope = DEFAULT_SCOPE | ||
scope = DEFAULT_SCOPE, | ||
): AbstractInjector<TChildContext<TContext, R, Token>> { | ||
@@ -115,3 +145,6 @@ this.throwIfDisposed(token); | ||
public resolve<Token extends keyof TContext>(token: Token, target?: Function): TContext[Token] { | ||
public resolve<Token extends keyof TContext>( | ||
token: Token, | ||
target?: Function, | ||
): TContext[Token] { | ||
this.throwIfDisposed(token); | ||
@@ -133,2 +166,6 @@ return this.resolveInternal(token, target); | ||
public createChildInjector(): Injector<TContext> { | ||
return new ChildInjector(this); | ||
} | ||
public async dispose() { | ||
@@ -148,3 +185,6 @@ if (!this.isDisposed) { | ||
protected abstract resolveInternal<Token extends keyof TContext>(token: Token, target?: Function): TContext[Token]; | ||
protected abstract resolveInternal<Token extends keyof TContext>( | ||
token: Token, | ||
target?: Function, | ||
): TContext[Token]; | ||
} | ||
@@ -154,2 +194,3 @@ | ||
public override resolveInternal(token: never): never { | ||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions | ||
throw new Error(`No provider found for "${token}"!.`); | ||
@@ -162,14 +203,17 @@ } | ||
abstract class ChildInjector<TParentContext, TProvided, CurrentToken extends string> extends AbstractInjector< | ||
TChildContext<TParentContext, TProvided, CurrentToken> | ||
> { | ||
private cached: { value?: any } | undefined; | ||
private readonly disposables = new Set<Disposable>(); | ||
constructor(protected readonly parent: AbstractInjector<TParentContext>, protected readonly token: CurrentToken, private readonly scope: Scope) { | ||
class ChildInjector< | ||
TParentContext, | ||
TContext, | ||
> extends AbstractInjector<TContext> { | ||
protected override async disposeInjectedValues(): Promise<void> {} | ||
protected override resolveInternal<Token extends keyof TContext>( | ||
token: Token, | ||
target?: Function, | ||
): TContext[Token] { | ||
return this.parent.resolve(token as any, target) as any; | ||
} | ||
constructor(protected readonly parent: AbstractInjector<TParentContext>) { | ||
super(); | ||
} | ||
protected abstract result(target: Function | undefined): TProvided; | ||
public override async dispose() { | ||
@@ -179,11 +223,33 @@ this.parent.removeChild(this as Injector<any>); | ||
} | ||
} | ||
protected override async disposeInjectedValues() { | ||
const promisesToAwait = [...this.disposables.values()].map((disposable) => disposable.dispose()); | ||
await Promise.all(promisesToAwait); | ||
abstract class ChildWithProvidedInjector< | ||
TParentContext, | ||
TProvided, | ||
CurrentToken extends string, | ||
> extends ChildInjector< | ||
TParentContext, | ||
TChildContext<TParentContext, TProvided, CurrentToken> | ||
> { | ||
private cached: { value?: any } | undefined; | ||
private readonly disposables = new Set<Disposable>(); | ||
constructor( | ||
parent: AbstractInjector<TParentContext>, | ||
protected readonly token: CurrentToken, | ||
private readonly scope: Scope, | ||
) { | ||
super(parent); | ||
} | ||
protected override resolveInternal<SearchToken extends keyof TChildContext<TParentContext, TProvided, CurrentToken>>( | ||
protected abstract result(target: Function | undefined): TProvided; | ||
protected override resolveInternal< | ||
SearchToken extends keyof TChildContext< | ||
TParentContext, | ||
TProvided, | ||
CurrentToken | ||
>, | ||
>( | ||
token: SearchToken, | ||
target: Function | undefined | ||
target: Function | undefined, | ||
): TChildContext<TParentContext, TProvided, CurrentToken>[SearchToken] { | ||
@@ -219,6 +285,21 @@ if (token === this.token) { | ||
} | ||
protected override async disposeInjectedValues() { | ||
const promisesToAwait = [...this.disposables.values()].map((disposable) => | ||
disposable.dispose(), | ||
); | ||
await Promise.all(promisesToAwait); | ||
} | ||
} | ||
class ValueProvider<TParentContext, TProvided, ProvidedToken extends string> extends ChildInjector<TParentContext, TProvided, ProvidedToken> { | ||
constructor(parent: AbstractInjector<TParentContext>, token: ProvidedToken, private readonly value: TProvided) { | ||
class ValueProvider< | ||
TParentContext, | ||
TProvided, | ||
ProvidedToken extends string, | ||
> extends ChildWithProvidedInjector<TParentContext, TProvided, ProvidedToken> { | ||
constructor( | ||
parent: AbstractInjector<TParentContext>, | ||
token: ProvidedToken, | ||
private readonly value: TProvided, | ||
) { | ||
super(parent, token, Scope.Transient); | ||
@@ -231,7 +312,8 @@ } | ||
class FactoryProvider<TParentContext, TProvided, ProvidedToken extends string, Tokens extends InjectionToken<TParentContext>[]> extends ChildInjector< | ||
class FactoryProvider< | ||
TParentContext, | ||
TProvided, | ||
ProvidedToken | ||
> { | ||
ProvidedToken extends string, | ||
Tokens extends InjectionToken<TParentContext>[], | ||
> extends ChildWithProvidedInjector<TParentContext, TProvided, ProvidedToken> { | ||
constructor( | ||
@@ -241,3 +323,7 @@ parent: AbstractInjector<TParentContext>, | ||
scope: Scope, | ||
private readonly injectable: InjectableFunction<TParentContext, TProvided, Tokens> | ||
private readonly injectable: InjectableFunction< | ||
TParentContext, | ||
TProvided, | ||
Tokens | ||
>, | ||
) { | ||
@@ -247,11 +333,14 @@ super(parent, token, scope); | ||
protected override result(target: Function): TProvided { | ||
return this.registerProvidedValue(this.parent.injectFunction(this.injectable, target)); | ||
return this.registerProvidedValue( | ||
this.parent.injectFunction(this.injectable, target), | ||
); | ||
} | ||
} | ||
class ClassProvider<TParentContext, TProvided, ProvidedToken extends string, Tokens extends InjectionToken<TParentContext>[]> extends ChildInjector< | ||
class ClassProvider< | ||
TParentContext, | ||
TProvided, | ||
ProvidedToken | ||
> { | ||
ProvidedToken extends string, | ||
Tokens extends InjectionToken<TParentContext>[], | ||
> extends ChildWithProvidedInjector<TParentContext, TProvided, ProvidedToken> { | ||
constructor( | ||
@@ -261,3 +350,7 @@ parent: AbstractInjector<TParentContext>, | ||
scope: Scope, | ||
private readonly injectable: InjectableClass<TParentContext, TProvided, Tokens> | ||
private readonly injectable: InjectableClass< | ||
TParentContext, | ||
TProvided, | ||
Tokens | ||
>, | ||
) { | ||
@@ -267,3 +360,5 @@ super(parent, token, scope); | ||
protected override result(target: Function): TProvided { | ||
return this.registerProvidedValue(this.parent.injectClass(this.injectable, target)); | ||
return this.registerProvidedValue( | ||
this.parent.injectClass(this.injectable, target), | ||
); | ||
} | ||
@@ -270,0 +365,0 @@ } |
{ | ||
"extends": "../tsconfig.settings.json", | ||
"extends": "../tsconfig.base.json", | ||
"compilerOptions": { | ||
@@ -4,0 +4,0 @@ "rootDir": ".", |
import { Disposable } from './api/Disposable.js'; | ||
export function isDisposable(maybeDisposable: unknown): maybeDisposable is Disposable { | ||
export function isDisposable( | ||
maybeDisposable: unknown, | ||
): maybeDisposable is Disposable { | ||
const asDisposable = maybeDisposable as Disposable; | ||
return asDisposable && asDisposable.dispose && typeof asDisposable.dispose === 'function'; | ||
return ( | ||
asDisposable && | ||
asDisposable.dispose && | ||
typeof asDisposable.dispose === 'function' | ||
); | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
102880
1051
686
24