From 0de0a7228b081fe16dacd601e34935c2529c8e16 Mon Sep 17 00:00:00 2001 From: Wenzhao Hu Date: Fri, 1 Mar 2024 10:39:24 +0800 Subject: [PATCH] chore: reformat code --- .editorconfig | 2 +- src/decorators.ts | 154 +- src/dependencyCollection.ts | 372 ++--- src/dependencyDeclare.ts | 24 +- src/dependencyDescriptor.ts | 98 +- src/dependencyForwardRef.ts | 24 +- src/dependencyIdentifier.ts | 32 +- src/dependencyItem.ts | 116 +- src/dependencyLookUp.ts | 34 +- src/dependencyQuantity.ts | 122 +- src/dependencyWithNew.ts | 26 +- src/dispose.ts | 4 +- src/error.ts | 6 +- src/idleValue.ts | 158 +- src/injector.ts | 1362 ++++++++-------- src/publicApi.ts | 44 +- src/react-bindings/reactComponent.tsx | 86 +- src/react-bindings/reactContext.tsx | 18 +- src/react-bindings/reactDecorators.ts | 56 +- src/react-bindings/reactHooks.tsx | 70 +- src/react-bindings/reactRx.tsx | 120 +- src/types.ts | 10 +- test/async/async.base.ts | 6 +- test/async/async.item.ts | 48 +- test/core.spec.ts | 2116 ++++++++++++------------- test/react.spec.tsx | 252 +-- test/rx.spec.tsx | 510 +++--- test/util/expectToThrow.ts | 16 +- 28 files changed, 2943 insertions(+), 2943 deletions(-) diff --git a/.editorconfig b/.editorconfig index 5e9218b..935c7f8 100644 --- a/.editorconfig +++ b/.editorconfig @@ -6,7 +6,7 @@ root = true # Tab indentation [*] end_of_line = lf -indent_style = tab +indent_style = space trim_trailing_whitespace = true indent_size = 2 diff --git a/src/decorators.ts b/src/decorators.ts index 5b51421..309a2f4 100644 --- a/src/decorators.ts +++ b/src/decorators.ts @@ -1,8 +1,8 @@ import { DependencyDescriptor } from './dependencyDescriptor' import { - DependencyIdentifier, - IdentifierDecorator, - IdentifierDecoratorSymbol, + DependencyIdentifier, + IdentifierDecorator, + IdentifierDecoratorSymbol, } from './dependencyIdentifier' import { Ctor, prettyPrintIdentifier } from './dependencyItem' import { LookUp, Quantity } from './types' @@ -12,88 +12,88 @@ export const TARGET = Symbol('$$TARGET') export const DEPENDENCIES = Symbol('$$DEPENDENCIES') class DependencyDescriptorNotFoundError extends RediError { - constructor(index: number, target: Ctor) { - const msg = `Could not find dependency registered on the ${index} (indexed) parameter of the constructor of "${prettyPrintIdentifier( - target - )}".` + constructor(index: number, target: Ctor) { + const msg = `Could not find dependency registered on the ${index} (indexed) parameter of the constructor of "${prettyPrintIdentifier( + target + )}".` - super(msg) - } + super(msg) + } } export class IdentifierUndefinedError extends RediError { - constructor(target: Ctor, index: number) { - const msg = `It seems that you register "undefined" as dependency on the ${ - index + 1 - } parameter of "${prettyPrintIdentifier( - target - )}". Please make sure that there is not cyclic dependency among your TypeScript files, or consider using "forwardRef". For more info please visit our website https://redi.wendell.fun/docs/debug#could-not-find-dependency-registered-on` - - super(msg) - } + constructor(target: Ctor, index: number) { + const msg = `It seems that you register "undefined" as dependency on the ${ + index + 1 + } parameter of "${prettyPrintIdentifier( + target + )}". Please make sure that there is not cyclic dependency among your TypeScript files, or consider using "forwardRef". For more info please visit our website https://redi.wendell.fun/docs/debug#could-not-find-dependency-registered-on` + + super(msg) + } } /** * @internal */ export function getDependencies( - registerTarget: Ctor + registerTarget: Ctor ): DependencyDescriptor[] { - const target = registerTarget as any - return target[DEPENDENCIES] || [] + const target = registerTarget as any + return target[DEPENDENCIES] || [] } /** * @internal */ export function getDependencyByIndex( - registerTarget: Ctor, - index: number + registerTarget: Ctor, + index: number ): DependencyDescriptor { - const allDependencies = getDependencies(registerTarget) - const dep = allDependencies.find( - (descriptor) => descriptor.paramIndex === index - ) + const allDependencies = getDependencies(registerTarget) + const dep = allDependencies.find( + (descriptor) => descriptor.paramIndex === index + ) - if (!dep) { - throw new DependencyDescriptorNotFoundError(index, registerTarget) - } + if (!dep) { + throw new DependencyDescriptorNotFoundError(index, registerTarget) + } - return dep + return dep } /** * @internal */ export function setDependency( - registerTarget: Ctor, - identifier: DependencyIdentifier, - paramIndex: number, - quantity: Quantity = Quantity.REQUIRED, - lookUp?: LookUp + registerTarget: Ctor, + identifier: DependencyIdentifier, + paramIndex: number, + quantity: Quantity = Quantity.REQUIRED, + lookUp?: LookUp ): void { - const descriptor: DependencyDescriptor = { - paramIndex, - identifier, - quantity, - lookUp, - withNew: false, - } - - // sometimes identifier could be 'undefined' if user meant to pass in an ES class - // this is related to how classes are transpiled - if (typeof identifier === 'undefined') { - throw new IdentifierUndefinedError(registerTarget, paramIndex) - } - - const target = registerTarget as any - // deal with inheritance, subclass need to declare dependencies on its on - if (target[TARGET] === target) { - target[DEPENDENCIES].push(descriptor) - } else { - target[DEPENDENCIES] = [descriptor] - target[TARGET] = target - } + const descriptor: DependencyDescriptor = { + paramIndex, + identifier, + quantity, + lookUp, + withNew: false, + } + + // sometimes identifier could be 'undefined' if user meant to pass in an ES class + // this is related to how classes are transpiled + if (typeof identifier === 'undefined') { + throw new IdentifierUndefinedError(registerTarget, paramIndex) + } + + const target = registerTarget as any + // deal with inheritance, subclass need to declare dependencies on its on + if (target[TARGET] === target) { + target[DEPENDENCIES].push(descriptor) + } else { + target[DEPENDENCIES] = [descriptor] + target[TARGET] = target + } } const knownIdentifiers = new Set() @@ -105,24 +105,24 @@ const knownIdentifiers = new Set() * @returns Identifier that could also be used as a decorator */ export function createIdentifier(id: string): IdentifierDecorator { - if (knownIdentifiers.has(id)) { - throw new RediError(`Identifier "${id}" already exists.`) - } else { - knownIdentifiers.add(id) - } - - const decorator = (( - function (registerTarget: Ctor, _key: string, index: number): void { - setDependency(registerTarget, decorator, index) - } - )) as IdentifierDecorator // decorator as an identifier - - // TODO: @wzhudev should assign a name to the function so it would be easy to debug in inspect tools - // decorator.name = `[redi]: ${id}`; - decorator.toString = () => id - decorator[IdentifierDecoratorSymbol] = true - - return decorator + if (knownIdentifiers.has(id)) { + throw new RediError(`Identifier "${id}" already exists.`) + } else { + knownIdentifiers.add(id) + } + + const decorator = (( + function (registerTarget: Ctor, _key: string, index: number): void { + setDependency(registerTarget, decorator, index) + } + )) as IdentifierDecorator // decorator as an identifier + + // TODO: @wzhudev should assign a name to the function so it would be easy to debug in inspect tools + // decorator.name = `[redi]: ${id}`; + decorator.toString = () => id + decorator[IdentifierDecoratorSymbol] = true + + return decorator } /** @@ -130,5 +130,5 @@ export function createIdentifier(id: string): IdentifierDecorator { */ /* istanbul ignore next */ export function TEST_ONLY_clearKnownIdentifiers(): void { - knownIdentifiers.clear() + knownIdentifiers.clear() } diff --git a/src/dependencyCollection.ts b/src/dependencyCollection.ts index 35849b5..f0b49af 100644 --- a/src/dependencyCollection.ts +++ b/src/dependencyCollection.ts @@ -1,6 +1,6 @@ import { - DependencyIdentifier, - isIdentifierDecorator, + DependencyIdentifier, + isIdentifierDecorator, } from './dependencyIdentifier' import { Ctor, DependencyItem, prettyPrintIdentifier } from './dependencyItem' import { checkQuantity, retrieveQuantity } from './dependencyQuantity' @@ -12,45 +12,45 @@ export type DependencyPair = [DependencyIdentifier, DependencyItem] export type DependencyClass = [Ctor] export type Dependency = DependencyPair | DependencyClass export type DependencyWithInstance = [ - Ctor | DependencyIdentifier, - T + Ctor | DependencyIdentifier, + T ] export type DependencyOrInstance = - | Dependency - | DependencyWithInstance + | Dependency + | DependencyWithInstance export function isBareClassDependency( - thing: Dependency + thing: Dependency ): thing is DependencyClass { - return thing.length === 1 + return thing.length === 1 } export class DependencyNotFoundForModuleError extends RediError { - constructor( - toInstantiate: Ctor | DependencyIdentifier, - id: DependencyIdentifier, - index: number - ) { - const msg = `Cannot find "${prettyPrintIdentifier( - id - )}" registered by any injector. It is the ${index}th param of "${ - isIdentifierDecorator(toInstantiate) - ? prettyPrintIdentifier(toInstantiate) - : (toInstantiate as Ctor).name - }".` - - super(msg) - } + constructor( + toInstantiate: Ctor | DependencyIdentifier, + id: DependencyIdentifier, + index: number + ) { + const msg = `Cannot find "${prettyPrintIdentifier( + id + )}" registered by any injector. It is the ${index}th param of "${ + isIdentifierDecorator(toInstantiate) + ? prettyPrintIdentifier(toInstantiate) + : (toInstantiate as Ctor).name + }".` + + super(msg) + } } export class DependencyNotFoundError extends RediError { - constructor(id: DependencyIdentifier) { - const msg = `Cannot find "${prettyPrintIdentifier( - id - )}" registered by any injector.` + constructor(id: DependencyIdentifier) { + const msg = `Cannot find "${prettyPrintIdentifier( + id + )}" registered by any injector.` - super(msg) - } + super(msg) + } } /** @@ -59,102 +59,102 @@ export class DependencyNotFoundError extends RediError { * @internal */ export class DependencyCollection implements IDisposable { - private readonly dependencyMap = new Map< - DependencyIdentifier, - DependencyItem[] - >() - - constructor(dependencies: Dependency[]) { - this.normalizeDependencies(dependencies).map((pair) => - this.add(pair[0], pair[1]) - ) - } - - public add(ctor: Ctor): void - public add(id: DependencyIdentifier, val: DependencyItem): void - public add( - ctorOrId: Ctor | DependencyIdentifier, - val?: DependencyItem - ): void { - if (typeof val === 'undefined') { - val = { useClass: ctorOrId as Ctor, lazy: false } - } - - let arr = this.dependencyMap.get(ctorOrId) - if (typeof arr === 'undefined') { - arr = [] - this.dependencyMap.set(ctorOrId, arr) - } - arr.push(val) - } - - public delete(id: DependencyIdentifier): void { - this.dependencyMap.delete(id) - } - - public get(id: DependencyIdentifier): DependencyItem - public get( - id: DependencyIdentifier, - quantity: Quantity.REQUIRED - ): DependencyItem - public get( - id: DependencyIdentifier, - quantity: Quantity.MANY - ): DependencyItem[] - public get( - id: DependencyIdentifier, - quantity: Quantity.OPTIONAL - ): DependencyItem | null - public get( - id: DependencyIdentifier, - quantity: Quantity - ): DependencyItem | DependencyItem[] | null - public get( - id: DependencyIdentifier, - quantity: Quantity = Quantity.REQUIRED - ): DependencyItem | DependencyItem[] | null { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const ret = this.dependencyMap.get(id)! - - checkQuantity(id, quantity, ret.length) - return retrieveQuantity(quantity, ret) - } - - public has(id: DependencyIdentifier): boolean { - return this.dependencyMap.has(id) - } - - public append(dependencies: Dependency[]): void { - this.normalizeDependencies(dependencies).forEach((pair) => - this.add(pair[0], pair[1]) - ) - } - - public dispose(): void { - this.dependencyMap.clear() - } - - /** - * normalize dependencies to `DependencyItem` - */ - private normalizeDependencies( - dependencies: Dependency[] - ): DependencyPair[] { - return dependencies.map((dependency) => { - const id = dependency[0] - let val: DependencyItem - if (isBareClassDependency(dependency)) { - val = { - useClass: dependency[0], - lazy: false, - } - } else { - val = dependency[1] - } - - return [id, val] - }) - } + private readonly dependencyMap = new Map< + DependencyIdentifier, + DependencyItem[] + >() + + constructor(dependencies: Dependency[]) { + this.normalizeDependencies(dependencies).map((pair) => + this.add(pair[0], pair[1]) + ) + } + + public add(ctor: Ctor): void + public add(id: DependencyIdentifier, val: DependencyItem): void + public add( + ctorOrId: Ctor | DependencyIdentifier, + val?: DependencyItem + ): void { + if (typeof val === 'undefined') { + val = { useClass: ctorOrId as Ctor, lazy: false } + } + + let arr = this.dependencyMap.get(ctorOrId) + if (typeof arr === 'undefined') { + arr = [] + this.dependencyMap.set(ctorOrId, arr) + } + arr.push(val) + } + + public delete(id: DependencyIdentifier): void { + this.dependencyMap.delete(id) + } + + public get(id: DependencyIdentifier): DependencyItem + public get( + id: DependencyIdentifier, + quantity: Quantity.REQUIRED + ): DependencyItem + public get( + id: DependencyIdentifier, + quantity: Quantity.MANY + ): DependencyItem[] + public get( + id: DependencyIdentifier, + quantity: Quantity.OPTIONAL + ): DependencyItem | null + public get( + id: DependencyIdentifier, + quantity: Quantity + ): DependencyItem | DependencyItem[] | null + public get( + id: DependencyIdentifier, + quantity: Quantity = Quantity.REQUIRED + ): DependencyItem | DependencyItem[] | null { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const ret = this.dependencyMap.get(id)! + + checkQuantity(id, quantity, ret.length) + return retrieveQuantity(quantity, ret) + } + + public has(id: DependencyIdentifier): boolean { + return this.dependencyMap.has(id) + } + + public append(dependencies: Dependency[]): void { + this.normalizeDependencies(dependencies).forEach((pair) => + this.add(pair[0], pair[1]) + ) + } + + public dispose(): void { + this.dependencyMap.clear() + } + + /** + * normalize dependencies to `DependencyItem` + */ + private normalizeDependencies( + dependencies: Dependency[] + ): DependencyPair[] { + return dependencies.map((dependency) => { + const id = dependency[0] + let val: DependencyItem + if (isBareClassDependency(dependency)) { + val = { + useClass: dependency[0], + lazy: false, + } + } else { + val = dependency[1] + } + + return [id, val] + }) + } } /** @@ -163,65 +163,65 @@ export class DependencyCollection implements IDisposable { * @internal */ export class ResolvedDependencyCollection implements IDisposable { - private readonly resolvedDependencies = new Map< - DependencyIdentifier, - any[] - >() - - public add(id: DependencyIdentifier, val: T | null): void { - let arr = this.resolvedDependencies.get(id) - if (typeof arr === 'undefined') { - arr = [] - this.resolvedDependencies.set(id, arr) - } - - arr.push(val) - } - - public has(id: DependencyIdentifier): boolean { - return this.resolvedDependencies.has(id) - } - - public delete(id: DependencyIdentifier): void { - if (this.resolvedDependencies.has(id)) { - const things = this.resolvedDependencies.get(id)! - things.forEach((t) => (isDisposable(t) ? t.dispose() : void 0)) - this.resolvedDependencies.delete(id) - } - } - - public get(id: DependencyIdentifier): T - public get( - id: DependencyIdentifier, - quantity: Quantity.OPTIONAL - ): T | null - public get(id: DependencyIdentifier, quantity: Quantity.REQUIRED): T - public get(id: DependencyIdentifier, quantity: Quantity.MANY): T[] - public get(id: DependencyIdentifier, quantity: Quantity): T[] | T | null - public get( - id: DependencyIdentifier, - quantity: Quantity = Quantity.REQUIRED - ): T | T[] | null { - const ret = this.resolvedDependencies.get(id) - - if (!ret) { - throw new DependencyNotFoundError(id) - } - - checkQuantity(id, quantity, ret.length) - - if (quantity === Quantity.MANY) { - return ret - } else { - return ret[0] - } - } - - public dispose(): void { - Array.from(this.resolvedDependencies.values()).forEach((items) => { - items.forEach((item) => (isDisposable(item) ? item.dispose() : void 0)) - }) - - this.resolvedDependencies.clear() - } + private readonly resolvedDependencies = new Map< + DependencyIdentifier, + any[] + >() + + public add(id: DependencyIdentifier, val: T | null): void { + let arr = this.resolvedDependencies.get(id) + if (typeof arr === 'undefined') { + arr = [] + this.resolvedDependencies.set(id, arr) + } + + arr.push(val) + } + + public has(id: DependencyIdentifier): boolean { + return this.resolvedDependencies.has(id) + } + + public delete(id: DependencyIdentifier): void { + if (this.resolvedDependencies.has(id)) { + const things = this.resolvedDependencies.get(id)! + things.forEach((t) => (isDisposable(t) ? t.dispose() : void 0)) + this.resolvedDependencies.delete(id) + } + } + + public get(id: DependencyIdentifier): T + public get( + id: DependencyIdentifier, + quantity: Quantity.OPTIONAL + ): T | null + public get(id: DependencyIdentifier, quantity: Quantity.REQUIRED): T + public get(id: DependencyIdentifier, quantity: Quantity.MANY): T[] + public get(id: DependencyIdentifier, quantity: Quantity): T[] | T | null + public get( + id: DependencyIdentifier, + quantity: Quantity = Quantity.REQUIRED + ): T | T[] | null { + const ret = this.resolvedDependencies.get(id) + + if (!ret) { + throw new DependencyNotFoundError(id) + } + + checkQuantity(id, quantity, ret.length) + + if (quantity === Quantity.MANY) { + return ret + } else { + return ret[0] + } + } + + public dispose(): void { + Array.from(this.resolvedDependencies.values()).forEach((items) => { + items.forEach((item) => (isDisposable(item) ? item.dispose() : void 0)) + }) + + this.resolvedDependencies.clear() + } } diff --git a/src/dependencyDeclare.ts b/src/dependencyDeclare.ts index 9922678..4e79830 100644 --- a/src/dependencyDeclare.ts +++ b/src/dependencyDeclare.ts @@ -9,17 +9,17 @@ import { Ctor, FactoryDep } from './dependencyItem' * @param deps Dependencies */ export function setDependencies( - registerTarget: Ctor, - deps: FactoryDep[] + registerTarget: Ctor, + deps: FactoryDep[] ): void { - const normalizedDescriptors = normalizeFactoryDeps(deps) - normalizedDescriptors.forEach((descriptor) => { - setDependency( - registerTarget, - descriptor.identifier, - descriptor.paramIndex, - descriptor.quantity, - descriptor.lookUp - ) - }) + const normalizedDescriptors = normalizeFactoryDeps(deps) + normalizedDescriptors.forEach((descriptor) => { + setDependency( + registerTarget, + descriptor.identifier, + descriptor.paramIndex, + descriptor.quantity, + descriptor.lookUp + ) + }) } diff --git a/src/dependencyDescriptor.ts b/src/dependencyDescriptor.ts index fadc7e3..71dbe6d 100644 --- a/src/dependencyDescriptor.ts +++ b/src/dependencyDescriptor.ts @@ -7,68 +7,68 @@ import { RediError } from './error' import { LookUp, Quantity } from './types' export interface DependencyDescriptor { - paramIndex: number - identifier: DependencyIdentifier - quantity: Quantity - lookUp?: LookUp - withNew: boolean + paramIndex: number + identifier: DependencyIdentifier + quantity: Quantity + lookUp?: LookUp + withNew: boolean } /** * describes dependencies of a IDependencyItem */ export interface Dependencies { - dependencies: DependencyDescriptor[] + dependencies: DependencyDescriptor[] } export function normalizeFactoryDeps( - deps?: FactoryDep[] + deps?: FactoryDep[] ): DependencyDescriptor[] { - if (!deps) { - return [] - } + if (!deps) { + return [] + } - return deps.map((dep, index) => { - if (!Array.isArray(dep)) { - return { - paramIndex: index, - identifier: dep, - quantity: Quantity.REQUIRED, - withNew: false, - } - } + return deps.map((dep, index) => { + if (!Array.isArray(dep)) { + return { + paramIndex: index, + identifier: dep, + quantity: Quantity.REQUIRED, + withNew: false, + } + } - const modifiers = dep.slice(0, dep.length - 1) as FactoryDepModifier[] - const identifier = dep[dep.length - 1] as DependencyIdentifier + const modifiers = dep.slice(0, dep.length - 1) as FactoryDepModifier[] + const identifier = dep[dep.length - 1] as DependencyIdentifier - let lookUp: LookUp | undefined = undefined - let quantity = Quantity.REQUIRED - let withNew = false + let lookUp: LookUp | undefined = undefined + let quantity = Quantity.REQUIRED + let withNew = false - ;(modifiers as FactoryDepModifier[]).forEach( - (modifier: FactoryDepModifier) => { - if (modifier instanceof Self) { - lookUp = LookUp.SELF - } else if (modifier instanceof SkipSelf) { - lookUp = LookUp.SKIP_SELF - } else if (modifier instanceof Optional) { - quantity = Quantity.OPTIONAL - } else if (modifier instanceof Many) { - quantity = Quantity.MANY - } else if (modifier instanceof WithNew) { - withNew = true - } else { - throw new RediError(`unknown dep modifier ${modifier}.`) - } - } - ) + ;(modifiers as FactoryDepModifier[]).forEach( + (modifier: FactoryDepModifier) => { + if (modifier instanceof Self) { + lookUp = LookUp.SELF + } else if (modifier instanceof SkipSelf) { + lookUp = LookUp.SKIP_SELF + } else if (modifier instanceof Optional) { + quantity = Quantity.OPTIONAL + } else if (modifier instanceof Many) { + quantity = Quantity.MANY + } else if (modifier instanceof WithNew) { + withNew = true + } else { + throw new RediError(`unknown dep modifier ${modifier}.`) + } + } + ) - return { - paramIndex: index, - identifier: identifier as DependencyIdentifier, - quantity, - lookUp, - withNew, - } - }) + return { + paramIndex: index, + identifier: identifier as DependencyIdentifier, + quantity, + lookUp, + withNew, + } + }) } diff --git a/src/dependencyForwardRef.ts b/src/dependencyForwardRef.ts index a0d2398..8e9d8c4 100644 --- a/src/dependencyForwardRef.ts +++ b/src/dependencyForwardRef.ts @@ -1,29 +1,29 @@ import { - DependencyIdentifier, - NormalizedDependencyIdentifier, + DependencyIdentifier, + NormalizedDependencyIdentifier, } from './dependencyIdentifier' import { Ctor } from './dependencyItem' export interface ForwardRef { - unwrap(): Ctor + unwrap(): Ctor } export function forwardRef(wrapper: () => Ctor): ForwardRef { - return { - unwrap: wrapper, - } + return { + unwrap: wrapper, + } } export function isForwardRef(thing: unknown): thing is ForwardRef { - return !!thing && typeof (thing as any).unwrap === 'function' + return !!thing && typeof (thing as any).unwrap === 'function' } export function normalizeForwardRef( - id: DependencyIdentifier + id: DependencyIdentifier ): NormalizedDependencyIdentifier { - if (isForwardRef(id)) { - return id.unwrap() - } + if (isForwardRef(id)) { + return id.unwrap() + } - return id + return id } diff --git a/src/dependencyIdentifier.ts b/src/dependencyIdentifier.ts index 71424a0..6225b93 100644 --- a/src/dependencyIdentifier.ts +++ b/src/dependencyIdentifier.ts @@ -4,33 +4,33 @@ import { ForwardRef } from './dependencyForwardRef' export const IdentifierDecoratorSymbol = Symbol('$$IDENTIFIER_DECORATOR') export type IdentifierDecorator = { - [IdentifierDecoratorSymbol]: true + [IdentifierDecoratorSymbol]: true - // call signature of an decorator - (...args: any[]): void + // call signature of an decorator + (...args: any[]): void - /** - * beautify console - */ - toString(): string + /** + * beautify console + */ + toString(): string - type: T + type: T } // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types export function isIdentifierDecorator( - thing: any + thing: any ): thing is IdentifierDecorator { - return thing && thing[IdentifierDecoratorSymbol] === true + return thing && thing[IdentifierDecoratorSymbol] === true } export type DependencyIdentifier = - | string - | Ctor - | ForwardRef - | IdentifierDecorator + | string + | Ctor + | ForwardRef + | IdentifierDecorator export type NormalizedDependencyIdentifier = Exclude< - DependencyIdentifier, - ForwardRef + DependencyIdentifier, + ForwardRef > diff --git a/src/dependencyItem.ts b/src/dependencyItem.ts index 6a219b7..831ec04 100644 --- a/src/dependencyItem.ts +++ b/src/dependencyItem.ts @@ -1,119 +1,119 @@ import { - DependencyIdentifier, - IdentifierDecoratorSymbol, + DependencyIdentifier, + IdentifierDecoratorSymbol, } from './dependencyIdentifier' import { Self, SkipSelf } from './dependencyLookUp' import { Many, Optional } from './dependencyQuantity' import { WithNew } from './dependencyWithNew' export interface Ctor { - new (...args: any[]): T + new (...args: any[]): T - name: string + name: string } export function isCtor(thing: unknown): thing is Ctor { - return typeof thing === 'function' + return typeof thing === 'function' } export interface DependencyItemHooks { - onInstantiation?: (instance: T) => void + onInstantiation?: (instance: T) => void } export interface ClassDependencyItem extends DependencyItemHooks { - useClass: Ctor - lazy?: boolean + useClass: Ctor + lazy?: boolean } export function isClassDependencyItem( - thing: unknown + thing: unknown ): thing is ClassDependencyItem { - if (thing && typeof (thing as any).useClass !== 'undefined') { - return true - } + if (thing && typeof (thing as any).useClass !== 'undefined') { + return true + } - return false + return false } export type FactoryDepModifier = - | typeof Self - | typeof SkipSelf - | typeof Optional - | typeof Many - | typeof WithNew + | typeof Self + | typeof SkipSelf + | typeof Optional + | typeof Many + | typeof WithNew export type FactoryDep = - | [...FactoryDepModifier[], DependencyIdentifier] - | DependencyIdentifier + | [...FactoryDepModifier[], DependencyIdentifier] + | DependencyIdentifier export interface FactoryDependencyItem extends DependencyItemHooks { - useFactory: (...deps: any[]) => T - dynamic?: true - deps?: FactoryDep[] + useFactory: (...deps: any[]) => T + dynamic?: true + deps?: FactoryDep[] } export function isFactoryDependencyItem( - thing: unknown + thing: unknown ): thing is FactoryDependencyItem { - if (thing && typeof (thing as any).useFactory !== 'undefined') { - return true - } + if (thing && typeof (thing as any).useFactory !== 'undefined') { + return true + } - return false + return false } export interface ValueDependencyItem extends DependencyItemHooks { - useValue: T + useValue: T } export function isValueDependencyItem( - thing: unknown + thing: unknown ): thing is ValueDependencyItem { - if (thing && typeof (thing as any).useValue !== 'undefined') { - return true - } + if (thing && typeof (thing as any).useValue !== 'undefined') { + return true + } - return false + return false } export interface AsyncDependencyItem extends DependencyItemHooks { - useAsync: () => Promise< - T | Ctor | [DependencyIdentifier, SyncDependencyItem] - > + useAsync: () => Promise< + T | Ctor | [DependencyIdentifier, SyncDependencyItem] + > } export function isAsyncDependencyItem( - thing: unknown + thing: unknown ): thing is AsyncDependencyItem { - if (thing && typeof (thing as any).useAsync !== 'undefined') { - return true - } + if (thing && typeof (thing as any).useAsync !== 'undefined') { + return true + } - return false + return false } export const AsyncHookSymbol = Symbol('AsyncHook') export interface AsyncHook { - __symbol: typeof AsyncHookSymbol - whenReady(): Promise + __symbol: typeof AsyncHookSymbol + whenReady(): Promise } export function isAsyncHook(thing: unknown): thing is AsyncHook { - if (thing && (thing as any)['__symbol'] === AsyncHookSymbol) { - // FIXME@wzhudev: should not be undefined but a symbol here - return true - } + if (thing && (thing as any)['__symbol'] === AsyncHookSymbol) { + // FIXME@wzhudev: should not be undefined but a symbol here + return true + } - return false + return false } export type SyncDependencyItem = - | ClassDependencyItem - | FactoryDependencyItem - | ValueDependencyItem + | ClassDependencyItem + | FactoryDependencyItem + | ValueDependencyItem export type DependencyItem = SyncDependencyItem | AsyncDependencyItem export function prettyPrintIdentifier(id: DependencyIdentifier): string { - if (typeof id === 'undefined') { - return 'undefined' - } + if (typeof id === 'undefined') { + return 'undefined' + } - return isCtor(id) && !(id as any)[IdentifierDecoratorSymbol] - ? id.name - : id.toString() + return isCtor(id) && !(id as any)[IdentifierDecoratorSymbol] + ? id.name + : id.toString() } diff --git a/src/dependencyLookUp.ts b/src/dependencyLookUp.ts index 80f7e36..8da8324 100644 --- a/src/dependencyLookUp.ts +++ b/src/dependencyLookUp.ts @@ -3,38 +3,38 @@ import { Ctor } from './dependencyItem' import { LookUp } from './types' function changeLookup(target: Ctor, index: number, lookUp: LookUp) { - const descriptor = getDependencyByIndex(target, index) - descriptor.lookUp = lookUp + const descriptor = getDependencyByIndex(target, index) + descriptor.lookUp = lookUp } function lookupDecoratorFactoryProducer(lookUp: LookUp) { - return function DecoratorFactory(this: any) { - if (this instanceof DecoratorFactory) { - return this - } + return function DecoratorFactory(this: any) { + if (this instanceof DecoratorFactory) { + return this + } - return function (target: Ctor, _key: string, index: number) { - changeLookup(target, index, lookUp) - } - } as any + return function (target: Ctor, _key: string, index: number) { + changeLookup(target, index, lookUp) + } + } as any } interface SkipSelfDecorator { - (): any - // eslint-disable-next-line @typescript-eslint/no-misused-new - new (): SkipSelfDecorator + (): any + // eslint-disable-next-line @typescript-eslint/no-misused-new + new (): SkipSelfDecorator } /** * when resolving this dependency, skip the current injector */ export const SkipSelf: SkipSelfDecorator = lookupDecoratorFactoryProducer( - LookUp.SKIP_SELF + LookUp.SKIP_SELF ) interface SelfDecorator { - (): any - // eslint-disable-next-line @typescript-eslint/no-misused-new - new (): SelfDecorator + (): any + // eslint-disable-next-line @typescript-eslint/no-misused-new + new (): SelfDecorator } /** * when resolving this dependency, only search the current injector diff --git a/src/dependencyQuantity.ts b/src/dependencyQuantity.ts index b58a180..de3e626 100644 --- a/src/dependencyQuantity.ts +++ b/src/dependencyQuantity.ts @@ -1,7 +1,7 @@ import { - getDependencyByIndex, - IdentifierUndefinedError, - setDependency, + getDependencyByIndex, + IdentifierUndefinedError, + setDependency, } from './decorators' import { DependencyIdentifier } from './dependencyIdentifier' import { Ctor, prettyPrintIdentifier } from './dependencyItem' @@ -9,93 +9,93 @@ import { RediError } from './error' import { Quantity } from './types' class QuantityCheckError extends RediError { - constructor( - id: DependencyIdentifier, - quantity: Quantity, - actual: number - ) { - const msg = `Expect "${quantity}" dependency items for id "${prettyPrintIdentifier( - id - )}" but get ${actual}.` + constructor( + id: DependencyIdentifier, + quantity: Quantity, + actual: number + ) { + const msg = `Expect "${quantity}" dependency items for id "${prettyPrintIdentifier( + id + )}" but get ${actual}.` - super(msg) - } + super(msg) + } } export function checkQuantity( - id: DependencyIdentifier, - quantity: Quantity, - length: number + id: DependencyIdentifier, + quantity: Quantity, + length: number ): void { - if ( - (quantity === Quantity.OPTIONAL && length > 1) || - (quantity === Quantity.REQUIRED && length !== 1) - ) { - throw new QuantityCheckError(id, quantity, length) - } + if ( + (quantity === Quantity.OPTIONAL && length > 1) || + (quantity === Quantity.REQUIRED && length !== 1) + ) { + throw new QuantityCheckError(id, quantity, length) + } } export function retrieveQuantity(quantity: Quantity, arr: T[]): T[] | T { - if (quantity === Quantity.MANY) { - return arr - } else { - return arr[0] - } + if (quantity === Quantity.MANY) { + return arr + } else { + return arr[0] + } } function changeQuantity(target: Ctor, index: number, quantity: Quantity) { - const descriptor = getDependencyByIndex(target, index) - descriptor.quantity = quantity + const descriptor = getDependencyByIndex(target, index) + descriptor.quantity = quantity } function quantifyDecoratorFactoryProducer(quantity: Quantity) { - return function decoratorFactory( - // typescript would remove `this` after transpilation - // this line just declare the type of `this` - this: any, - id?: DependencyIdentifier - ) { - if (this instanceof decoratorFactory) { - return this - } + return function decoratorFactory( + // typescript would remove `this` after transpilation + // this line just declare the type of `this` + this: any, + id?: DependencyIdentifier + ) { + if (this instanceof decoratorFactory) { + return this + } - return function (registerTarget: Ctor, _key: string, index: number) { - if (id) { - setDependency(registerTarget, id, index, quantity) - } else { - if (quantity === Quantity.REQUIRED) { - throw new IdentifierUndefinedError(registerTarget, index) - } + return function (registerTarget: Ctor, _key: string, index: number) { + if (id) { + setDependency(registerTarget, id, index, quantity) + } else { + if (quantity === Quantity.REQUIRED) { + throw new IdentifierUndefinedError(registerTarget, index) + } - changeQuantity(registerTarget, index, quantity) - } - } - } as any + changeQuantity(registerTarget, index, quantity) + } + } + } as any } interface ManyDecorator { - (id?: DependencyIdentifier): any - // eslint-disable-next-line @typescript-eslint/no-misused-new - new (): ManyDecorator + (id?: DependencyIdentifier): any + // eslint-disable-next-line @typescript-eslint/no-misused-new + new (): ManyDecorator } export const Many: ManyDecorator = quantifyDecoratorFactoryProducer( - Quantity.MANY + Quantity.MANY ) interface OptionalDecorator { - (id?: DependencyIdentifier): any - // eslint-disable-next-line @typescript-eslint/no-misused-new - new (): OptionalDecorator + (id?: DependencyIdentifier): any + // eslint-disable-next-line @typescript-eslint/no-misused-new + new (): OptionalDecorator } export const Optional: OptionalDecorator = quantifyDecoratorFactoryProducer( - Quantity.OPTIONAL + Quantity.OPTIONAL ) interface InjectDecorator { - (id: DependencyIdentifier): any - // eslint-disable-next-line @typescript-eslint/no-misused-new - new (): InjectDecorator + (id: DependencyIdentifier): any + // eslint-disable-next-line @typescript-eslint/no-misused-new + new (): InjectDecorator } export const Inject: InjectDecorator = quantifyDecoratorFactoryProducer( - Quantity.REQUIRED + Quantity.REQUIRED ) diff --git a/src/dependencyWithNew.ts b/src/dependencyWithNew.ts index 8161239..8a2d990 100644 --- a/src/dependencyWithNew.ts +++ b/src/dependencyWithNew.ts @@ -2,26 +2,26 @@ import { getDependencyByIndex } from './decorators' import { Ctor } from './dependencyItem' function changeToSelf(target: Ctor, index: number, withNew: boolean) { - const descriptor = getDependencyByIndex(target, index) - descriptor.withNew = withNew + const descriptor = getDependencyByIndex(target, index) + descriptor.withNew = withNew } function withNewDecoratorFactoryProducer(withNew: boolean) { - return function DecoratorFactory(this: any) { - if (this instanceof DecoratorFactory) { - return this - } + return function DecoratorFactory(this: any) { + if (this instanceof DecoratorFactory) { + return this + } - return function (target: Ctor, _key: string, index: number) { - changeToSelf(target, index, withNew) - } - } as any + return function (target: Ctor, _key: string, index: number) { + changeToSelf(target, index, withNew) + } + } as any } interface ToSelfDecorator { - (): any - // eslint-disable-next-line @typescript-eslint/no-misused-new - new (): ToSelfDecorator + (): any + // eslint-disable-next-line @typescript-eslint/no-misused-new + new (): ToSelfDecorator } /** diff --git a/src/dispose.ts b/src/dispose.ts index eeb94e1..684c69e 100644 --- a/src/dispose.ts +++ b/src/dispose.ts @@ -1,7 +1,7 @@ export interface IDisposable { - dispose(): void + dispose(): void } export function isDisposable(thing: unknown): thing is IDisposable { - return !!thing && typeof (thing as any).dispose === 'function' + return !!thing && typeof (thing as any).dispose === 'function' } diff --git a/src/error.ts b/src/error.ts index 2fa9969..9e1723b 100644 --- a/src/error.ts +++ b/src/error.ts @@ -1,5 +1,5 @@ export class RediError extends Error { - constructor(message: string) { - super(`[redi]: ${message}`) - } + constructor(message: string) { + super(`[redi]: ${message}`) + } } diff --git a/src/idleValue.ts b/src/idleValue.ts index ab7bc92..4e2c9b4 100644 --- a/src/idleValue.ts +++ b/src/idleValue.ts @@ -1,8 +1,8 @@ import { IDisposable } from './dispose' export interface IdleDeadline { - readonly didTimeout: boolean - timeRemaining(): DOMHighResTimeStamp + readonly didTimeout: boolean + timeRemaining(): DOMHighResTimeStamp } export type DisposableCallback = () => void @@ -12,59 +12,59 @@ export type DisposableCallback = () => void * the browser doesn't support requestIdleCallback */ export let runWhenIdle: ( - callback: (idle?: IdleDeadline) => void, - timeout?: number + callback: (idle?: IdleDeadline) => void, + timeout?: number ) => DisposableCallback // declare global variables because apparently the type file doesn't have it, for now declare function requestIdleCallback( - callback: (args: IdleDeadline) => void, - options?: { timeout: number } + callback: (args: IdleDeadline) => void, + options?: { timeout: number } ): number declare function cancelIdleCallback(handle: number): void // use an IIFE to set up runWhenIdle ;(function () { - if ( - typeof requestIdleCallback !== 'undefined' && - typeof cancelIdleCallback !== 'undefined' - ) { - // use native requestIdleCallback - runWhenIdle = (runner, timeout?) => { - const handle: number = requestIdleCallback( - runner, - typeof timeout === 'number' ? { timeout } : undefined - ) - let disposed = false - return () => { - if (disposed) { - return - } - disposed = true - cancelIdleCallback(handle) - } - } - } else { - // use setTimeout as hack - const dummyIdle: IdleDeadline = Object.freeze({ - didTimeout: true, - timeRemaining() { - return 15 - }, - }) + if ( + typeof requestIdleCallback !== 'undefined' && + typeof cancelIdleCallback !== 'undefined' + ) { + // use native requestIdleCallback + runWhenIdle = (runner, timeout?) => { + const handle: number = requestIdleCallback( + runner, + typeof timeout === 'number' ? { timeout } : undefined + ) + let disposed = false + return () => { + if (disposed) { + return + } + disposed = true + cancelIdleCallback(handle) + } + } + } else { + // use setTimeout as hack + const dummyIdle: IdleDeadline = Object.freeze({ + didTimeout: true, + timeRemaining() { + return 15 + }, + }) - runWhenIdle = (runner) => { - const handle = setTimeout(() => runner(dummyIdle)) - let disposed = false - return () => { - if (disposed) { - return - } - disposed = true - clearTimeout(handle) - } - } - } + runWhenIdle = (runner) => { + const handle = setTimeout(() => runner(dummyIdle)) + let disposed = false + return () => { + if (disposed) { + return + } + disposed = true + clearTimeout(handle) + } + } + } })() /** @@ -73,44 +73,44 @@ declare function cancelIdleCallback(handle: number): void * the type of the returned value of the executor would be T */ export class IdleValue implements IDisposable { - private readonly selfExecutor: () => void - private readonly disposeCallback: () => void + private readonly selfExecutor: () => void + private readonly disposeCallback: () => void - private didRun = false - private value?: T - private error?: Error + private didRun = false + private value?: T + private error?: Error - constructor(executor: () => T) { - this.selfExecutor = () => { - try { - this.value = executor() - } catch (err: any) { - this.error = err - } finally { - this.didRun = true - } - } + constructor(executor: () => T) { + this.selfExecutor = () => { + try { + this.value = executor() + } catch (err: any) { + this.error = err + } finally { + this.didRun = true + } + } - this.disposeCallback = runWhenIdle(() => this.selfExecutor()) - } + this.disposeCallback = runWhenIdle(() => this.selfExecutor()) + } - hasRun(): boolean { - return this.didRun - } + hasRun(): boolean { + return this.didRun + } - dispose(): void { - this.disposeCallback() - } + dispose(): void { + this.disposeCallback() + } - getValue(): T { - if (!this.didRun) { - this.dispose() - this.selfExecutor() - } - if (this.error) { - throw this.error - } - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return this.value! - } + getValue(): T { + if (!this.didRun) { + this.dispose() + this.selfExecutor() + } + if (this.error) { + throw this.error + } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return this.value! + } } diff --git a/src/injector.ts b/src/injector.ts index 5d28aba..f62124c 100644 --- a/src/injector.ts +++ b/src/injector.ts @@ -1,31 +1,31 @@ import { getDependencies } from './decorators' import { - Dependency, - DependencyCollection, - DependencyNotFoundError, - DependencyNotFoundForModuleError, - DependencyOrInstance, - ResolvedDependencyCollection, + Dependency, + DependencyCollection, + DependencyNotFoundError, + DependencyNotFoundForModuleError, + DependencyOrInstance, + ResolvedDependencyCollection, } from './dependencyCollection' import { normalizeFactoryDeps } from './dependencyDescriptor' import { normalizeForwardRef } from './dependencyForwardRef' import { DependencyIdentifier } from './dependencyIdentifier' import { - AsyncDependencyItem, - AsyncHook, - ClassDependencyItem, - Ctor, - DependencyItem, - FactoryDependencyItem, - ValueDependencyItem, - isAsyncDependencyItem, - isAsyncHook, - isClassDependencyItem, - isCtor, - isFactoryDependencyItem, - isValueDependencyItem, - prettyPrintIdentifier, - AsyncHookSymbol, + AsyncDependencyItem, + AsyncHook, + ClassDependencyItem, + Ctor, + DependencyItem, + FactoryDependencyItem, + ValueDependencyItem, + isAsyncDependencyItem, + isAsyncHook, + isClassDependencyItem, + isCtor, + isFactoryDependencyItem, + isValueDependencyItem, + prettyPrintIdentifier, + AsyncHookSymbol, } from './dependencyItem' import { RediError } from './error' import { IdleValue } from './idleValue' @@ -36,688 +36,688 @@ const MAX_RESOLUTIONS_QUEUED = 300 const NotInstantiatedSymbol = Symbol('$$NOT_INSTANTIATED_SYMBOL') class CircularDependencyError extends RediError { - constructor(id: DependencyIdentifier) { - super( - `Detecting cyclic dependency. The last identifier is "${prettyPrintIdentifier( - id - )}".` - ) - } + constructor(id: DependencyIdentifier) { + super( + `Detecting cyclic dependency. The last identifier is "${prettyPrintIdentifier( + id + )}".` + ) + } } class InjectorAlreadyDisposedError extends RediError { - constructor() { - super('Injector cannot be accessed after it was disposed.') - } + constructor() { + super('Injector cannot be accessed after it was disposed.') + } } class AsyncItemReturnAsyncItemError extends RediError { - constructor(id: DependencyIdentifier) { - super( - `Async item "${prettyPrintIdentifier(id)}" returns another async item.` - ) - } + constructor(id: DependencyIdentifier) { + super( + `Async item "${prettyPrintIdentifier(id)}" returns another async item.` + ) + } } class GetAsyncItemFromSyncApiError extends RediError { - constructor(id: DependencyIdentifier) { - super(`Cannot get async item "${prettyPrintIdentifier(id)}" from sync api.`) - } + constructor(id: DependencyIdentifier) { + super(`Cannot get async item "${prettyPrintIdentifier(id)}" from sync api.`) + } } class AddDependencyAfterResolutionError extends RediError { - constructor(id: DependencyIdentifier) { - super( - `Cannot add dependency "${prettyPrintIdentifier( - id - )}" after it is already resolved.` - ) - } + constructor(id: DependencyIdentifier) { + super( + `Cannot add dependency "${prettyPrintIdentifier( + id + )}" after it is already resolved.` + ) + } } class DeleteDependencyAfterResolutionError extends RediError { - constructor(id: DependencyIdentifier) { - super( - `Cannot dependency dependency "${prettyPrintIdentifier( - id - )}" after it is already resolved.` - ) - } + constructor(id: DependencyIdentifier) { + super( + `Cannot dependency dependency "${prettyPrintIdentifier( + id + )}" after it is already resolved.` + ) + } } export interface IAccessor { - get: Injector['get'] + get: Injector['get'] } /** * */ export class Injector { - private readonly dependencyCollection: DependencyCollection - private readonly resolvedDependencyCollection: ResolvedDependencyCollection - - private readonly children: Injector[] = [] - - private resolutionOngoing = 0 - - private disposed = false - - /** - * Create a new `Injector` instance - * @param dependencies Dependencies that should be resolved by this injector instance. - * @param parent Optional parent injector. - */ - constructor( - dependencies?: Dependency[], - private readonly parent: Injector | null = null - ) { - this.dependencyCollection = new DependencyCollection(dependencies || []) - this.resolvedDependencyCollection = new ResolvedDependencyCollection() - - if (parent) { - parent.children.push(this) - } - } - - /** - * Create a child inject with a set of dependencies. - * @param dependencies Dependencies that should be resolved by the newly created child injector. - * @returns The child injector. - */ - public createChild(dependencies?: Dependency[]): Injector { - this.ensureInjectorNotDisposed() - return new Injector(dependencies, this) - } - - /** - * Dispose the injector and all dependencies held by this injector. Note that its child injectors will dispose first. - */ - public dispose(): void { - // Dispose child injectors first. - this.children.forEach((c) => c.dispose()) - this.children.length = 0 - - // Call `dispose` method on each instantiated dependencies if they are `IDisposable` and clear collections. - this.dependencyCollection.dispose() - this.resolvedDependencyCollection.dispose() - - this.deleteSelfFromParent() - - this.disposed = true - } - - private deleteSelfFromParent(): void { - if (this.parent) { - const index = this.parent.children.indexOf(this) - if (index > -1) { - this.parent.children.splice(index, 1) - } - } - } - - /** - * Add a dependency or its instance into injector. It would throw an error if the dependency - * has already been instantiated. - * - * @param dependency The dependency or an instance that would be add in the injector. - */ - public add(dependency: DependencyOrInstance): void { - this.ensureInjectorNotDisposed() - const identifierOrCtor = dependency[0] - const item = dependency[1] - - if (this.resolvedDependencyCollection.has(identifierOrCtor)) { - throw new AddDependencyAfterResolutionError(identifierOrCtor) - } - - if (typeof item === 'undefined') { - // Add dependency - this.dependencyCollection.add(identifierOrCtor as Ctor) - } else if ( - isAsyncDependencyItem(item) || - isClassDependencyItem(item) || - isValueDependencyItem(item) || - isFactoryDependencyItem(item) - ) { - // Add dependency - this.dependencyCollection.add(identifierOrCtor, item as DependencyItem) - } else { - // Add instance - this.resolvedDependencyCollection.add(identifierOrCtor, item as T) - } - } - - /** - * Replace an injection mapping for interface-based injection. It would throw an error if the dependency - * has already been instantiated. - * - * @param dependency The dependency that will replace the already existed dependency. - */ - public replace(dependency: Dependency): void { - this.ensureInjectorNotDisposed() - - const identifier = dependency[0] - if (this.resolvedDependencyCollection.has(identifier)) { - throw new AddDependencyAfterResolutionError(identifier) - } - - this.dependencyCollection.delete(identifier) - if (dependency.length === 1) { - this.dependencyCollection.add(identifier as Ctor) - } else { - this.dependencyCollection.add(identifier, dependency[1]) - } - } - - /** - * Delete a dependency from an injector. It would throw an error when the deleted dependency - * has already been instantiated. - * - * @param identifier The identifier of the dependency that is supposed to be deleted. - */ - public delete(identifier: DependencyIdentifier): void { - this.ensureInjectorNotDisposed() - - if (this.resolvedDependencyCollection.has(identifier)) { - throw new DeleteDependencyAfterResolutionError(identifier) - } - - this.dependencyCollection.delete(identifier) - } - - /** - * Invoke a function with dependencies injected. The function could only get dependency from the injector - * and other methods are not accessible for the function. - * - * @param cb the function to be executed - * @param args arguments to be passed into the function - * @returns the return value of the function - */ - invoke( - cb: (accessor: IAccessor, ...args: P) => T, - ...args: P - ): T { - const accessor: IAccessor = { - get: ( - id: DependencyIdentifier, - quantityOrLookup?: Quantity | LookUp, - lookUp?: LookUp - ) => { - return this._get(id, quantityOrLookup, lookUp) - }, - } - - return cb(accessor, ...args) - } - - /** - * Check if the injector could initialize a dependency. - * - * @param id Identifier of the dependency - */ - public has(id: DependencyIdentifier): boolean { - return this.dependencyCollection.has(id) || this.parent?.has(id) || false - } - - public get(id: DependencyIdentifier, lookUp?: LookUp): T - public get( - id: DependencyIdentifier, - quantity: Quantity.MANY, - lookUp?: LookUp - ): T[] - public get( - id: DependencyIdentifier, - quantity: Quantity.OPTIONAL, - lookUp?: LookUp - ): T | null - public get( - id: DependencyIdentifier, - quantity: Quantity.REQUIRED, - lookUp?: LookUp - ): T - public get( - id: DependencyIdentifier, - quantity?: Quantity, - lookUp?: LookUp - ): T[] | T | null - public get( - id: DependencyIdentifier, - quantityOrLookup?: Quantity | LookUp, - lookUp?: LookUp - ): T[] | T | null - /** - * Get dependency instance(s). - * - * @param id Identifier of the dependency - * @param quantityOrLookup @link{Quantity} or @link{LookUp} - * @param lookUp @link{LookUp} - */ - public get( - id: DependencyIdentifier, - quantityOrLookup?: Quantity | LookUp, - lookUp?: LookUp - ): T[] | T | null { - const newResult = this._get(id, quantityOrLookup, lookUp) - - if ( - (Array.isArray(newResult) && newResult.some((r) => isAsyncHook(r))) || - isAsyncHook(newResult) - ) { - throw new GetAsyncItemFromSyncApiError(id) - } - - return newResult as T | T[] | null - } - - private _get( - id: DependencyIdentifier, - quantityOrLookup?: Quantity | LookUp, - lookUp?: LookUp, - toSelf?: boolean - ): T[] | T | AsyncHook | null { - this.ensureInjectorNotDisposed() - - let quantity: Quantity = Quantity.REQUIRED - if ( - quantityOrLookup === Quantity.REQUIRED || - quantityOrLookup === Quantity.OPTIONAL || - quantityOrLookup === Quantity.MANY - ) { - quantity = quantityOrLookup as Quantity - } else { - lookUp = quantityOrLookup as LookUp - } - - if (!toSelf) { - // see if the dependency is already resolved, return it and check quantity - const cachedResult = this.getValue(id, quantity, lookUp) - if (cachedResult !== NotInstantiatedSymbol) { - return cachedResult - } - } - - // see if the dependency can be instantiated by itself or its parent - return this.createDependency(id, quantity, lookUp, !toSelf) as - | T[] - | T - | AsyncHook - | null - } - - /** - * Get a dependency in the async way. - */ - public getAsync(id: DependencyIdentifier): Promise { - this.ensureInjectorNotDisposed() - - const cachedResult = this.getValue(id, Quantity.REQUIRED) - if (cachedResult !== NotInstantiatedSymbol) { - return Promise.resolve(cachedResult as T) - } - - const newResult = this.createDependency(id, Quantity.REQUIRED) - if (!isAsyncHook(newResult)) { - return Promise.resolve(newResult as T) - } - - return newResult.whenReady() - } - - /** - * Instantiate a class. The created instance would not be held by the injector. - */ - public createInstance( - ctor: new (...args: [...T, ...U]) => C, - ...customArgs: T - ): C { - this.ensureInjectorNotDisposed() - - return this._resolveClass(ctor as Ctor, ...customArgs) - } - - private resolveDependency( - id: DependencyIdentifier, - item: DependencyItem, - shouldCache = true - ): T | AsyncHook { - if (isValueDependencyItem(item)) { - return this.resolveValueDependency(id, item as ValueDependencyItem) - } else if (isFactoryDependencyItem(item)) { - return this.resolveFactory( - id, - item as FactoryDependencyItem, - shouldCache - ) - } else if (isClassDependencyItem(item)) { - return this.resolveClass(id, item as ClassDependencyItem, shouldCache) - } else { - return this.resolveAsync(id, item as AsyncDependencyItem) - } - } - - private resolveValueDependency( - id: DependencyIdentifier, - item: ValueDependencyItem - ): T { - const thing = item.useValue - this.resolvedDependencyCollection.add(id, thing) - return thing - } - - private resolveClass( - id: DependencyIdentifier | null, - item: ClassDependencyItem, - shouldCache = true - ): T { - const ctor = item.useClass - let thing: T - - if (item.lazy) { - const idle = new IdleValue(() => this._resolveClass(ctor)) - thing = new Proxy(Object.create(null), { - get(target: any, key: string | number | symbol): any { - if (key in target) { - return target[key] // such as toString - } - - // hack checking if it's a async loader - if (key === 'whenReady') { - return undefined - } - - const hasInstantiated = idle.hasRun() - const thing = idle.getValue() - if (!hasInstantiated) { - item.onInstantiation?.(thing) - } - - let property = (thing as any)[key] - if (typeof property !== 'function') { - return property - } - - property = property.bind(thing) - target[key] = property - - return property - }, - set(_target: any, key: string | number | symbol, value: any): boolean { - ;(idle.getValue() as any)[key] = value - return true - }, - }) - } else { - thing = this._resolveClass(ctor) - } - - if (id && shouldCache) { - this.resolvedDependencyCollection.add(id, thing) - } - - return thing - } - - private _resolveClass(ctor: Ctor, ...extraParams: any[]) { - this.markNewResolution(ctor) - - const declaredDependencies = getDependencies(ctor) - .sort((a, b) => a.paramIndex - b.paramIndex) - .map((descriptor) => ({ - ...descriptor, - identifier: normalizeForwardRef(descriptor.identifier), - })) - - const resolvedArgs: any[] = [] - - for (const dep of declaredDependencies) { - // recursive happens here - try { - const thing = this._get( - dep.identifier, - dep.quantity, - dep.lookUp, - dep.withNew - ) - resolvedArgs.push(thing) - } catch (error: unknown) { - if (error instanceof DependencyNotFoundError) { - throw new DependencyNotFoundForModuleError( - ctor, - dep.identifier, - dep.paramIndex - ) - } - - throw error - } - } - - let args = [...extraParams] - const firstDependencyArgIndex = - declaredDependencies.length > 0 - ? declaredDependencies[0].paramIndex - : args.length - - if (args.length !== firstDependencyArgIndex) { - console.warn( - `[redi]: Expect ${firstDependencyArgIndex} custom parameter(s) of ${ctor.toString()} but get ${ - args.length - }.` - ) - - const delta = firstDependencyArgIndex - args.length - if (delta > 0) { - args = [...args, ...new Array(delta).fill(undefined)] - } else { - args = args.slice(0, firstDependencyArgIndex) - } - } - - const thing = new ctor(...args, ...resolvedArgs) - - this.markResolutionCompleted() - - return thing - } - - private resolveFactory( - id: DependencyIdentifier, - item: FactoryDependencyItem, - shouldCache: boolean - ): T { - this.markNewResolution(id) - - const declaredDependencies = normalizeFactoryDeps(item.deps) - - const resolvedArgs: any[] = [] - for (const dep of declaredDependencies) { - try { - const thing = this._get( - dep.identifier, - dep.quantity, - dep.lookUp, - dep.withNew - ) - resolvedArgs.push(thing) - } catch (error: unknown) { - if (error instanceof DependencyNotFoundError) { - throw new DependencyNotFoundForModuleError( - id, - dep.identifier, - dep.paramIndex - ) - } - - throw error - } - } - - const thing = item.useFactory.apply(null, resolvedArgs) - - if (shouldCache) { - this.resolvedDependencyCollection.add(id, thing) - } - - this.markResolutionCompleted() - - item?.onInstantiation?.(thing) - - return thing - } - - private resolveAsync( - id: DependencyIdentifier, - item: AsyncDependencyItem - ): AsyncHook { - const asyncLoader: AsyncHook = { - __symbol: AsyncHookSymbol, - whenReady: () => this._resolveAsync(id, item), - } - return asyncLoader - } - - private _resolveAsync( - id: DependencyIdentifier, - item: AsyncDependencyItem - ): Promise { - return item.useAsync().then((thing) => { - // check if another promise has been resolved, - // do not resolve the async item twice - const resolvedCheck = this.getValue(id) - if (resolvedCheck !== NotInstantiatedSymbol) { - return resolvedCheck as T - } - - let ret: T - if (Array.isArray(thing)) { - const item = thing[1] - if (isAsyncDependencyItem(item)) { - throw new AsyncItemReturnAsyncItemError(id) - } else { - ret = this.resolveDependency(id, item) as T - } - } else if (isCtor(thing)) { - ret = this._resolveClass(thing) - } else { - ret = thing - } - - this.resolvedDependencyCollection.add(id, ret) - - return ret - }) - } - - private getValue( - id: DependencyIdentifier, - quantity: Quantity = Quantity.REQUIRED, - lookUp?: LookUp - ): null | T | T[] | typeof NotInstantiatedSymbol { - const onSelf = () => { - if ( - this.dependencyCollection.has(id) && - !this.resolvedDependencyCollection.has(id) - ) { - return NotInstantiatedSymbol - } - - return this.resolvedDependencyCollection.get(id, quantity) - } - - const onParent = () => { - if (this.parent) { - return this.parent.getValue(id, quantity) - } else { - return NotInstantiatedSymbol - } - } - - if (lookUp === LookUp.SKIP_SELF) { - return onParent() - } - - if (lookUp === LookUp.SELF) { - return onSelf() - } - - if ( - this.resolvedDependencyCollection.has(id) || - this.dependencyCollection.has(id) - ) { - return onSelf() - } - - return onParent() - } - - private createDependency( - id: DependencyIdentifier, - quantity: Quantity = Quantity.REQUIRED, - lookUp?: LookUp, - shouldCache = true - ): null | T | T[] | AsyncHook | (T | AsyncHook)[] { - const onSelf = () => { - const registrations = this.dependencyCollection.get(id, quantity) - - let ret: (T | AsyncHook)[] | T | AsyncHook | null = null - if (Array.isArray(registrations)) { - ret = registrations.map((dependencyItem) => - this.resolveDependency(id, dependencyItem, shouldCache) - ) - } else if (registrations) { - ret = this.resolveDependency(id, registrations, shouldCache) - } - - return ret - } - - const onParent = () => { - if (this.parent) { - return this.parent.createDependency( - id, - quantity, - undefined, - shouldCache - ) - } else { - if (quantity === Quantity.OPTIONAL) { - return null - } - - throw new DependencyNotFoundError(id) - } - } - - if (lookUp === LookUp.SKIP_SELF) { - return onParent() - } - - if ((id as any as Ctor) === Injector) { - return this as any as T - } - - if (this.dependencyCollection.has(id)) { - return onSelf() - } - - return onParent() - } - - private markNewResolution(id: DependencyIdentifier): void { - this.resolutionOngoing += 1 - - if (this.resolutionOngoing >= MAX_RESOLUTIONS_QUEUED) { - throw new CircularDependencyError(id) - } - } - - private markResolutionCompleted(): void { - this.resolutionOngoing -= 1 - } - - private ensureInjectorNotDisposed(): void { - if (this.disposed) { - throw new InjectorAlreadyDisposedError() - } - } + private readonly dependencyCollection: DependencyCollection + private readonly resolvedDependencyCollection: ResolvedDependencyCollection + + private readonly children: Injector[] = [] + + private resolutionOngoing = 0 + + private disposed = false + + /** + * Create a new `Injector` instance + * @param dependencies Dependencies that should be resolved by this injector instance. + * @param parent Optional parent injector. + */ + constructor( + dependencies?: Dependency[], + private readonly parent: Injector | null = null + ) { + this.dependencyCollection = new DependencyCollection(dependencies || []) + this.resolvedDependencyCollection = new ResolvedDependencyCollection() + + if (parent) { + parent.children.push(this) + } + } + + /** + * Create a child inject with a set of dependencies. + * @param dependencies Dependencies that should be resolved by the newly created child injector. + * @returns The child injector. + */ + public createChild(dependencies?: Dependency[]): Injector { + this.ensureInjectorNotDisposed() + return new Injector(dependencies, this) + } + + /** + * Dispose the injector and all dependencies held by this injector. Note that its child injectors will dispose first. + */ + public dispose(): void { + // Dispose child injectors first. + this.children.forEach((c) => c.dispose()) + this.children.length = 0 + + // Call `dispose` method on each instantiated dependencies if they are `IDisposable` and clear collections. + this.dependencyCollection.dispose() + this.resolvedDependencyCollection.dispose() + + this.deleteSelfFromParent() + + this.disposed = true + } + + private deleteSelfFromParent(): void { + if (this.parent) { + const index = this.parent.children.indexOf(this) + if (index > -1) { + this.parent.children.splice(index, 1) + } + } + } + + /** + * Add a dependency or its instance into injector. It would throw an error if the dependency + * has already been instantiated. + * + * @param dependency The dependency or an instance that would be add in the injector. + */ + public add(dependency: DependencyOrInstance): void { + this.ensureInjectorNotDisposed() + const identifierOrCtor = dependency[0] + const item = dependency[1] + + if (this.resolvedDependencyCollection.has(identifierOrCtor)) { + throw new AddDependencyAfterResolutionError(identifierOrCtor) + } + + if (typeof item === 'undefined') { + // Add dependency + this.dependencyCollection.add(identifierOrCtor as Ctor) + } else if ( + isAsyncDependencyItem(item) || + isClassDependencyItem(item) || + isValueDependencyItem(item) || + isFactoryDependencyItem(item) + ) { + // Add dependency + this.dependencyCollection.add(identifierOrCtor, item as DependencyItem) + } else { + // Add instance + this.resolvedDependencyCollection.add(identifierOrCtor, item as T) + } + } + + /** + * Replace an injection mapping for interface-based injection. It would throw an error if the dependency + * has already been instantiated. + * + * @param dependency The dependency that will replace the already existed dependency. + */ + public replace(dependency: Dependency): void { + this.ensureInjectorNotDisposed() + + const identifier = dependency[0] + if (this.resolvedDependencyCollection.has(identifier)) { + throw new AddDependencyAfterResolutionError(identifier) + } + + this.dependencyCollection.delete(identifier) + if (dependency.length === 1) { + this.dependencyCollection.add(identifier as Ctor) + } else { + this.dependencyCollection.add(identifier, dependency[1]) + } + } + + /** + * Delete a dependency from an injector. It would throw an error when the deleted dependency + * has already been instantiated. + * + * @param identifier The identifier of the dependency that is supposed to be deleted. + */ + public delete(identifier: DependencyIdentifier): void { + this.ensureInjectorNotDisposed() + + if (this.resolvedDependencyCollection.has(identifier)) { + throw new DeleteDependencyAfterResolutionError(identifier) + } + + this.dependencyCollection.delete(identifier) + } + + /** + * Invoke a function with dependencies injected. The function could only get dependency from the injector + * and other methods are not accessible for the function. + * + * @param cb the function to be executed + * @param args arguments to be passed into the function + * @returns the return value of the function + */ + invoke( + cb: (accessor: IAccessor, ...args: P) => T, + ...args: P + ): T { + const accessor: IAccessor = { + get: ( + id: DependencyIdentifier, + quantityOrLookup?: Quantity | LookUp, + lookUp?: LookUp + ) => { + return this._get(id, quantityOrLookup, lookUp) + }, + } + + return cb(accessor, ...args) + } + + /** + * Check if the injector could initialize a dependency. + * + * @param id Identifier of the dependency + */ + public has(id: DependencyIdentifier): boolean { + return this.dependencyCollection.has(id) || this.parent?.has(id) || false + } + + public get(id: DependencyIdentifier, lookUp?: LookUp): T + public get( + id: DependencyIdentifier, + quantity: Quantity.MANY, + lookUp?: LookUp + ): T[] + public get( + id: DependencyIdentifier, + quantity: Quantity.OPTIONAL, + lookUp?: LookUp + ): T | null + public get( + id: DependencyIdentifier, + quantity: Quantity.REQUIRED, + lookUp?: LookUp + ): T + public get( + id: DependencyIdentifier, + quantity?: Quantity, + lookUp?: LookUp + ): T[] | T | null + public get( + id: DependencyIdentifier, + quantityOrLookup?: Quantity | LookUp, + lookUp?: LookUp + ): T[] | T | null + /** + * Get dependency instance(s). + * + * @param id Identifier of the dependency + * @param quantityOrLookup @link{Quantity} or @link{LookUp} + * @param lookUp @link{LookUp} + */ + public get( + id: DependencyIdentifier, + quantityOrLookup?: Quantity | LookUp, + lookUp?: LookUp + ): T[] | T | null { + const newResult = this._get(id, quantityOrLookup, lookUp) + + if ( + (Array.isArray(newResult) && newResult.some((r) => isAsyncHook(r))) || + isAsyncHook(newResult) + ) { + throw new GetAsyncItemFromSyncApiError(id) + } + + return newResult as T | T[] | null + } + + private _get( + id: DependencyIdentifier, + quantityOrLookup?: Quantity | LookUp, + lookUp?: LookUp, + toSelf?: boolean + ): T[] | T | AsyncHook | null { + this.ensureInjectorNotDisposed() + + let quantity: Quantity = Quantity.REQUIRED + if ( + quantityOrLookup === Quantity.REQUIRED || + quantityOrLookup === Quantity.OPTIONAL || + quantityOrLookup === Quantity.MANY + ) { + quantity = quantityOrLookup as Quantity + } else { + lookUp = quantityOrLookup as LookUp + } + + if (!toSelf) { + // see if the dependency is already resolved, return it and check quantity + const cachedResult = this.getValue(id, quantity, lookUp) + if (cachedResult !== NotInstantiatedSymbol) { + return cachedResult + } + } + + // see if the dependency can be instantiated by itself or its parent + return this.createDependency(id, quantity, lookUp, !toSelf) as + | T[] + | T + | AsyncHook + | null + } + + /** + * Get a dependency in the async way. + */ + public getAsync(id: DependencyIdentifier): Promise { + this.ensureInjectorNotDisposed() + + const cachedResult = this.getValue(id, Quantity.REQUIRED) + if (cachedResult !== NotInstantiatedSymbol) { + return Promise.resolve(cachedResult as T) + } + + const newResult = this.createDependency(id, Quantity.REQUIRED) + if (!isAsyncHook(newResult)) { + return Promise.resolve(newResult as T) + } + + return newResult.whenReady() + } + + /** + * Instantiate a class. The created instance would not be held by the injector. + */ + public createInstance( + ctor: new (...args: [...T, ...U]) => C, + ...customArgs: T + ): C { + this.ensureInjectorNotDisposed() + + return this._resolveClass(ctor as Ctor, ...customArgs) + } + + private resolveDependency( + id: DependencyIdentifier, + item: DependencyItem, + shouldCache = true + ): T | AsyncHook { + if (isValueDependencyItem(item)) { + return this.resolveValueDependency(id, item as ValueDependencyItem) + } else if (isFactoryDependencyItem(item)) { + return this.resolveFactory( + id, + item as FactoryDependencyItem, + shouldCache + ) + } else if (isClassDependencyItem(item)) { + return this.resolveClass(id, item as ClassDependencyItem, shouldCache) + } else { + return this.resolveAsync(id, item as AsyncDependencyItem) + } + } + + private resolveValueDependency( + id: DependencyIdentifier, + item: ValueDependencyItem + ): T { + const thing = item.useValue + this.resolvedDependencyCollection.add(id, thing) + return thing + } + + private resolveClass( + id: DependencyIdentifier | null, + item: ClassDependencyItem, + shouldCache = true + ): T { + const ctor = item.useClass + let thing: T + + if (item.lazy) { + const idle = new IdleValue(() => this._resolveClass(ctor)) + thing = new Proxy(Object.create(null), { + get(target: any, key: string | number | symbol): any { + if (key in target) { + return target[key] // such as toString + } + + // hack checking if it's a async loader + if (key === 'whenReady') { + return undefined + } + + const hasInstantiated = idle.hasRun() + const thing = idle.getValue() + if (!hasInstantiated) { + item.onInstantiation?.(thing) + } + + let property = (thing as any)[key] + if (typeof property !== 'function') { + return property + } + + property = property.bind(thing) + target[key] = property + + return property + }, + set(_target: any, key: string | number | symbol, value: any): boolean { + ;(idle.getValue() as any)[key] = value + return true + }, + }) + } else { + thing = this._resolveClass(ctor) + } + + if (id && shouldCache) { + this.resolvedDependencyCollection.add(id, thing) + } + + return thing + } + + private _resolveClass(ctor: Ctor, ...extraParams: any[]) { + this.markNewResolution(ctor) + + const declaredDependencies = getDependencies(ctor) + .sort((a, b) => a.paramIndex - b.paramIndex) + .map((descriptor) => ({ + ...descriptor, + identifier: normalizeForwardRef(descriptor.identifier), + })) + + const resolvedArgs: any[] = [] + + for (const dep of declaredDependencies) { + // recursive happens here + try { + const thing = this._get( + dep.identifier, + dep.quantity, + dep.lookUp, + dep.withNew + ) + resolvedArgs.push(thing) + } catch (error: unknown) { + if (error instanceof DependencyNotFoundError) { + throw new DependencyNotFoundForModuleError( + ctor, + dep.identifier, + dep.paramIndex + ) + } + + throw error + } + } + + let args = [...extraParams] + const firstDependencyArgIndex = + declaredDependencies.length > 0 + ? declaredDependencies[0].paramIndex + : args.length + + if (args.length !== firstDependencyArgIndex) { + console.warn( + `[redi]: Expect ${firstDependencyArgIndex} custom parameter(s) of ${ctor.toString()} but get ${ + args.length + }.` + ) + + const delta = firstDependencyArgIndex - args.length + if (delta > 0) { + args = [...args, ...new Array(delta).fill(undefined)] + } else { + args = args.slice(0, firstDependencyArgIndex) + } + } + + const thing = new ctor(...args, ...resolvedArgs) + + this.markResolutionCompleted() + + return thing + } + + private resolveFactory( + id: DependencyIdentifier, + item: FactoryDependencyItem, + shouldCache: boolean + ): T { + this.markNewResolution(id) + + const declaredDependencies = normalizeFactoryDeps(item.deps) + + const resolvedArgs: any[] = [] + for (const dep of declaredDependencies) { + try { + const thing = this._get( + dep.identifier, + dep.quantity, + dep.lookUp, + dep.withNew + ) + resolvedArgs.push(thing) + } catch (error: unknown) { + if (error instanceof DependencyNotFoundError) { + throw new DependencyNotFoundForModuleError( + id, + dep.identifier, + dep.paramIndex + ) + } + + throw error + } + } + + const thing = item.useFactory.apply(null, resolvedArgs) + + if (shouldCache) { + this.resolvedDependencyCollection.add(id, thing) + } + + this.markResolutionCompleted() + + item?.onInstantiation?.(thing) + + return thing + } + + private resolveAsync( + id: DependencyIdentifier, + item: AsyncDependencyItem + ): AsyncHook { + const asyncLoader: AsyncHook = { + __symbol: AsyncHookSymbol, + whenReady: () => this._resolveAsync(id, item), + } + return asyncLoader + } + + private _resolveAsync( + id: DependencyIdentifier, + item: AsyncDependencyItem + ): Promise { + return item.useAsync().then((thing) => { + // check if another promise has been resolved, + // do not resolve the async item twice + const resolvedCheck = this.getValue(id) + if (resolvedCheck !== NotInstantiatedSymbol) { + return resolvedCheck as T + } + + let ret: T + if (Array.isArray(thing)) { + const item = thing[1] + if (isAsyncDependencyItem(item)) { + throw new AsyncItemReturnAsyncItemError(id) + } else { + ret = this.resolveDependency(id, item) as T + } + } else if (isCtor(thing)) { + ret = this._resolveClass(thing) + } else { + ret = thing + } + + this.resolvedDependencyCollection.add(id, ret) + + return ret + }) + } + + private getValue( + id: DependencyIdentifier, + quantity: Quantity = Quantity.REQUIRED, + lookUp?: LookUp + ): null | T | T[] | typeof NotInstantiatedSymbol { + const onSelf = () => { + if ( + this.dependencyCollection.has(id) && + !this.resolvedDependencyCollection.has(id) + ) { + return NotInstantiatedSymbol + } + + return this.resolvedDependencyCollection.get(id, quantity) + } + + const onParent = () => { + if (this.parent) { + return this.parent.getValue(id, quantity) + } else { + return NotInstantiatedSymbol + } + } + + if (lookUp === LookUp.SKIP_SELF) { + return onParent() + } + + if (lookUp === LookUp.SELF) { + return onSelf() + } + + if ( + this.resolvedDependencyCollection.has(id) || + this.dependencyCollection.has(id) + ) { + return onSelf() + } + + return onParent() + } + + private createDependency( + id: DependencyIdentifier, + quantity: Quantity = Quantity.REQUIRED, + lookUp?: LookUp, + shouldCache = true + ): null | T | T[] | AsyncHook | (T | AsyncHook)[] { + const onSelf = () => { + const registrations = this.dependencyCollection.get(id, quantity) + + let ret: (T | AsyncHook)[] | T | AsyncHook | null = null + if (Array.isArray(registrations)) { + ret = registrations.map((dependencyItem) => + this.resolveDependency(id, dependencyItem, shouldCache) + ) + } else if (registrations) { + ret = this.resolveDependency(id, registrations, shouldCache) + } + + return ret + } + + const onParent = () => { + if (this.parent) { + return this.parent.createDependency( + id, + quantity, + undefined, + shouldCache + ) + } else { + if (quantity === Quantity.OPTIONAL) { + return null + } + + throw new DependencyNotFoundError(id) + } + } + + if (lookUp === LookUp.SKIP_SELF) { + return onParent() + } + + if ((id as any as Ctor) === Injector) { + return this as any as T + } + + if (this.dependencyCollection.has(id)) { + return onSelf() + } + + return onParent() + } + + private markNewResolution(id: DependencyIdentifier): void { + this.resolutionOngoing += 1 + + if (this.resolutionOngoing >= MAX_RESOLUTIONS_QUEUED) { + throw new CircularDependencyError(id) + } + } + + private markResolutionCompleted(): void { + this.resolutionOngoing -= 1 + } + + private ensureInjectorNotDisposed(): void { + if (this.disposed) { + throw new InjectorAlreadyDisposedError() + } + } } diff --git a/src/publicApi.ts b/src/publicApi.ts index 829d79a..20f301b 100644 --- a/src/publicApi.ts +++ b/src/publicApi.ts @@ -6,42 +6,42 @@ export { Injector, IAccessor } from './injector' export { SkipSelf, Self } from './dependencyLookUp' export { DependencyPair, Dependency } from './dependencyCollection' export { - DependencyIdentifier, - IdentifierDecorator, + DependencyIdentifier, + IdentifierDecorator, } from './dependencyIdentifier' export { IDisposable } from './dispose' export { setDependencies } from './dependencyDeclare' export { WithNew } from './dependencyWithNew' export { - AsyncDependencyItem, - AsyncHook, - ClassDependencyItem, - Ctor, - DependencyItem, - FactoryDependencyItem, - isAsyncDependencyItem, - isAsyncHook, - isClassDependencyItem, - isCtor, - isFactoryDependencyItem, - isValueDependencyItem, - SyncDependencyItem, - ValueDependencyItem, + AsyncDependencyItem, + AsyncHook, + ClassDependencyItem, + Ctor, + DependencyItem, + FactoryDependencyItem, + isAsyncDependencyItem, + isAsyncHook, + isClassDependencyItem, + isCtor, + isFactoryDependencyItem, + isValueDependencyItem, + SyncDependencyItem, + ValueDependencyItem, } from './dependencyItem' export { RediError } from './error' const globalObject: any = - (typeof globalThis !== 'undefined' && globalThis) || - (typeof window !== 'undefined' && window) || - // @ts-ignore - (typeof global !== 'undefined' && global) + (typeof globalThis !== 'undefined' && globalThis) || + (typeof window !== 'undefined' && window) || + // @ts-ignore + (typeof global !== 'undefined' && global) const __REDI_GLOBAL_LOCK__ = 'REDI_GLOBAL_LOCK' if (globalObject[__REDI_GLOBAL_LOCK__]) { - console.error(`[redi]: You are loading scripts of redi more than once! This may cause undesired behavior in your application. + console.error(`[redi]: You are loading scripts of redi more than once! This may cause undesired behavior in your application. Maybe your dependencies added redi as its dependency and bundled redi to its dist files. Or you import different versions of redi. For more info please visit our website: https://redi.wendell.fun/en-US/docs/debug#import-scripts-of-redi-more-than-once`) } else { - globalObject[__REDI_GLOBAL_LOCK__] = true + globalObject[__REDI_GLOBAL_LOCK__] = true } diff --git a/src/react-bindings/reactComponent.tsx b/src/react-bindings/reactComponent.tsx index ae582d0..c6326ef 100644 --- a/src/react-bindings/reactComponent.tsx +++ b/src/react-bindings/reactComponent.tsx @@ -4,37 +4,37 @@ import { Injector, Dependency } from '@wendellhu/redi' import { RediProvider, RediConsumer } from './reactContext' function RediInjector( - props: React.PropsWithChildren<{ dependencies: Dependency[] }> + props: React.PropsWithChildren<{ dependencies: Dependency[] }> ) { - const { children, dependencies } = props - const childInjectorRef = React.useRef(null) + const { children, dependencies } = props + const childInjectorRef = React.useRef(null) - // dispose the injector when the container Injector unmounts - React.useEffect(() => () => childInjectorRef.current?.dispose(), []) + // dispose the injector when the container Injector unmounts + React.useEffect(() => () => childInjectorRef.current?.dispose(), []) - return ( - - {(context: { injector: Injector | null }) => { - let childInjector: Injector + return ( + + {(context: { injector: Injector | null }) => { + let childInjector: Injector - if (childInjectorRef.current) { - childInjector = childInjectorRef.current - } else { - childInjector = context.injector - ? context.injector.createChild(dependencies) - : new Injector(dependencies) + if (childInjectorRef.current) { + childInjector = childInjectorRef.current + } else { + childInjector = context.injector + ? context.injector.createChild(dependencies) + : new Injector(dependencies) - childInjectorRef.current = childInjector - } + childInjectorRef.current = childInjector + } - return ( - - {children} - - ) - }} - - ) + return ( + + {children} + + ) + }} + + ) } /** @@ -43,27 +43,27 @@ function RediInjector( * @returns */ export function connectInjector

( - Comp: React.ComponentType

, - injector: Injector + Comp: React.ComponentType

, + injector: Injector ): React.ComponentType

{ - return function ComponentWithInjector(props: P) { - return ( - - - - ) - } + return function ComponentWithInjector(props: P) { + return ( + + + + ) + } } export function connectDependencies

( - Comp: React.ComponentType

, - dependencies: Dependency[] + Comp: React.ComponentType

, + dependencies: Dependency[] ): React.ComponentType

{ - return function ComponentWithInjector(props: P) { - return ( - - - - ) - } + return function ComponentWithInjector(props: P) { + return ( + + + + ) + } } diff --git a/src/react-bindings/reactContext.tsx b/src/react-bindings/reactContext.tsx index 376886b..e1564e9 100644 --- a/src/react-bindings/reactContext.tsx +++ b/src/react-bindings/reactContext.tsx @@ -2,27 +2,27 @@ import * as React from 'react' import { Injector, RediError } from '@wendellhu/redi' declare global { - interface Window { - RediContextCreated: string | null - } + interface Window { + RediContextCreated: string | null + } } const RediContextCreated = '__RediContextCreated__' if (!window.RediContextCreated) { - window.RediContextCreated = RediContextCreated + window.RediContextCreated = RediContextCreated } else { - throw new RediError( - '"RediContext" is already created. You may import "RediContext" from different paths. Use "import { RediContext } from \'@wendellhu/redi/react-bindings\'; instead."' - ) + throw new RediError( + '"RediContext" is already created. You may import "RediContext" from different paths. Use "import { RediContext } from \'@wendellhu/redi/react-bindings\'; instead."' + ) } export interface IRediContext { - injector: Injector | null + injector: Injector | null } export const RediContext = React.createContext({ - injector: null, + injector: null, }) RediContext.displayName = 'RediContext' diff --git a/src/react-bindings/reactDecorators.ts b/src/react-bindings/reactDecorators.ts index ac7d531..cb60e05 100644 --- a/src/react-bindings/reactDecorators.ts +++ b/src/react-bindings/reactDecorators.ts @@ -1,41 +1,41 @@ import { - DependencyIdentifier, - Quantity, - LookUp, - RediError, + DependencyIdentifier, + Quantity, + LookUp, + RediError, } from '@wendellhu/redi' import { IRediContext } from './reactContext' class ClassComponentNotInRediContextError extends RediError { - constructor(component: React.Component) { - super( - `You should make "RediContext" as ${component.constructor.name}'s default context type. ` + - 'If you want to use multiple context, please check this on React doc site. ' + - 'https://reactjs.org/docs/context.html#classcontexttype' - ) - } + constructor(component: React.Component) { + super( + `You should make "RediContext" as ${component.constructor.name}'s default context type. ` + + 'If you want to use multiple context, please check this on React doc site. ' + + 'https://reactjs.org/docs/context.html#classcontexttype' + ) + } } export function WithDependency( - id: DependencyIdentifier, - quantity?: Quantity, - lookUp?: LookUp + id: DependencyIdentifier, + quantity?: Quantity, + lookUp?: LookUp ): any { - return function () { - return { - get(): T | T[] | null { - const thisComponent: React.Component = this as any + return function () { + return { + get(): T | T[] | null { + const thisComponent: React.Component = this as any - const context = thisComponent.context as IRediContext | null - if (!context || !context.injector) { - throw new ClassComponentNotInRediContextError(thisComponent) - } + const context = thisComponent.context as IRediContext | null + if (!context || !context.injector) { + throw new ClassComponentNotInRediContextError(thisComponent) + } - const injector = context.injector - const thing = injector.get(id, quantity || Quantity.REQUIRED, lookUp) + const injector = context.injector + const thing = injector.get(id, quantity || Quantity.REQUIRED, lookUp) - return thing - }, - } - } + return thing + }, + } + } } diff --git a/src/react-bindings/reactHooks.tsx b/src/react-bindings/reactHooks.tsx index 166fd2e..0f4b66a 100644 --- a/src/react-bindings/reactHooks.tsx +++ b/src/react-bindings/reactHooks.tsx @@ -1,63 +1,63 @@ import * as React from 'react' import { - DependencyIdentifier, - Injector, - LookUp, - Quantity, - RediError, + DependencyIdentifier, + Injector, + LookUp, + Quantity, + RediError, } from '@wendellhu/redi' import { RediContext } from './reactContext' class HooksNotInRediContextError extends RediError { - constructor() { - super('Using dependency injection outside of a RediContext.') - } + constructor() { + super('Using dependency injection outside of a RediContext.') + } } export function useInjector(): Injector { - const injectionContext = React.useContext(RediContext) - if (!injectionContext.injector) { - throw new HooksNotInRediContextError() - } + const injectionContext = React.useContext(RediContext) + if (!injectionContext.injector) { + throw new HooksNotInRediContextError() + } - return injectionContext.injector + return injectionContext.injector } export function useDependency( - id: DependencyIdentifier, - lookUp?: LookUp + id: DependencyIdentifier, + lookUp?: LookUp ): T export function useDependency( - id: DependencyIdentifier, - quantity: Quantity.MANY, - lookUp?: LookUp + id: DependencyIdentifier, + quantity: Quantity.MANY, + lookUp?: LookUp ): T[] export function useDependency( - id: DependencyIdentifier, - quantity: Quantity.OPTIONAL, - lookUp?: LookUp + id: DependencyIdentifier, + quantity: Quantity.OPTIONAL, + lookUp?: LookUp ): T | null export function useDependency( - id: DependencyIdentifier, - quantity: Quantity.REQUIRED, - lookUp?: LookUp + id: DependencyIdentifier, + quantity: Quantity.REQUIRED, + lookUp?: LookUp ): T export function useDependency( - id: DependencyIdentifier, - quantity: Quantity, - lookUp?: LookUp + id: DependencyIdentifier, + quantity: Quantity, + lookUp?: LookUp ): T | T[] | null export function useDependency( - id: DependencyIdentifier, - quantity?: Quantity, - lookUp?: LookUp + id: DependencyIdentifier, + quantity?: Quantity, + lookUp?: LookUp ): T | T[] | null export function useDependency( - id: DependencyIdentifier, - quantityOrLookUp?: Quantity | LookUp, - lookUp?: LookUp + id: DependencyIdentifier, + quantityOrLookUp?: Quantity | LookUp, + lookUp?: LookUp ): T | T[] | null { - const injector = useInjector() - return injector.get(id, quantityOrLookUp, lookUp) + const injector = useInjector() + return injector.get(id, quantityOrLookUp, lookUp) } diff --git a/src/react-bindings/reactRx.tsx b/src/react-bindings/reactRx.tsx index 93b5270..9acaf2d 100644 --- a/src/react-bindings/reactRx.tsx +++ b/src/react-bindings/reactRx.tsx @@ -1,13 +1,13 @@ import React, { - useEffect, - useState, - createContext, - useMemo, - useContext, - useCallback, - ReactNode, - Context, - useRef, + useEffect, + useState, + createContext, + useMemo, + useContext, + useCallback, + ReactNode, + Context, + useRef, } from 'react' import { BehaviorSubject, Observable } from 'rxjs' @@ -26,21 +26,21 @@ import { RediError } from '@wendellhu/redi' * `useDependencyContextValue` instead. */ export function useDependencyValue( - depValue$: Observable, - defaultValue?: T + depValue$: Observable, + defaultValue?: T ): T | undefined { - const firstValue: T | undefined = - depValue$ instanceof BehaviorSubject && typeof defaultValue === 'undefined' - ? depValue$.getValue() - : defaultValue - const [value, setValue] = useState(firstValue) + const firstValue: T | undefined = + depValue$ instanceof BehaviorSubject && typeof defaultValue === 'undefined' + ? depValue$.getValue() + : defaultValue + const [value, setValue] = useState(firstValue) - useEffect(() => { - const subscription = depValue$.subscribe((val: T) => setValue(val)) - return () => subscription.unsubscribe() - }, [depValue$]) + useEffect(() => { + const subscription = depValue$.subscribe((val: T) => setValue(val)) + return () => subscription.unsubscribe() + }, [depValue$]) - return value + return value } /** @@ -49,12 +49,12 @@ export function useDependencyValue( * @param update$ a signal that the data the functional component depends has updated */ export function useUpdateBinder(update$: Observable): void { - const [, dumpSet] = useState(0) + const [, dumpSet] = useState(0) - useEffect(() => { - const subscription = update$.subscribe(() => dumpSet((prev) => prev + 1)) - return () => subscription.unsubscribe() - }, []) + useEffect(() => { + const subscription = update$.subscribe(() => dumpSet((prev) => prev + 1)) + return () => subscription.unsubscribe() + }, []) } const DepValueMapProvider = new WeakMap, Context>() @@ -64,49 +64,49 @@ const DepValueMapProvider = new WeakMap, Context>() * it child component won't have to subscribe again and cause unnecessary */ export function useDependencyContext( - depValue$: Observable, - defaultValue?: T + depValue$: Observable, + defaultValue?: T ): { - Provider: (props: { initialState?: T; children: ReactNode }) => JSX.Element - value: T | undefined + Provider: (props: { initialState?: T; children: ReactNode }) => JSX.Element + value: T | undefined } { - const depRef = useRef | undefined>(undefined) - const value = useDependencyValue(depValue$, defaultValue) - const Context = useMemo(() => { - return createContext(value) - }, [depValue$]) - const Provider = useCallback( - (props: { initialState?: T; children: ReactNode }) => { - return {props.children} - }, - [depValue$, value] - ) + const depRef = useRef | undefined>(undefined) + const value = useDependencyValue(depValue$, defaultValue) + const Context = useMemo(() => { + return createContext(value) + }, [depValue$]) + const Provider = useCallback( + (props: { initialState?: T; children: ReactNode }) => { + return {props.children} + }, + [depValue$, value] + ) - if (depRef.current !== depValue$) { - if (depRef.current) { - DepValueMapProvider.delete(depRef.current) - } + if (depRef.current !== depValue$) { + if (depRef.current) { + DepValueMapProvider.delete(depRef.current) + } - depRef.current = depValue$ - DepValueMapProvider.set(depValue$, Context) - } + depRef.current = depValue$ + DepValueMapProvider.set(depValue$, Context) + } - return { - Provider, - value, - } + return { + Provider, + value, + } } export function useDependencyContextValue( - depValue$: Observable + depValue$: Observable ): T | undefined { - const context = DepValueMapProvider.get(depValue$) + const context = DepValueMapProvider.get(depValue$) - if (!context) { - throw new RediError( - `try to read context value but no ancestor component subscribed it.` - ) - } + if (!context) { + throw new RediError( + `try to read context value but no ancestor component subscribed it.` + ) + } - return useContext(context) + return useContext(context) } diff --git a/src/types.ts b/src/types.ts index 301213d..48f834b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,10 +1,10 @@ export enum Quantity { - MANY = 'many', - OPTIONAL = 'optional', - REQUIRED = 'required', + MANY = 'many', + OPTIONAL = 'optional', + REQUIRED = 'required', } export enum LookUp { - SELF = 'self', - SKIP_SELF = 'skipSelf', + SELF = 'self', + SKIP_SELF = 'skipSelf', } diff --git a/test/async/async.base.ts b/test/async/async.base.ts index 61f44ac..ce87234 100644 --- a/test/async/async.base.ts +++ b/test/async/async.base.ts @@ -1,12 +1,12 @@ import { createIdentifier } from '@wendellhu/redi' export class AA { - key = 'aa' + key = 'aa' } export interface BB { - key: string - getConstructedTime?(): number + key: string + getConstructedTime?(): number } export const bbI = createIdentifier('bb') diff --git a/test/async/async.item.ts b/test/async/async.item.ts index 58da945..a0cfd9e 100644 --- a/test/async/async.item.ts +++ b/test/async/async.item.ts @@ -3,40 +3,40 @@ import { DependencyPair, Inject } from '@wendellhu/redi' import { AA, BB, bbI } from './async.base' export class BBImpl implements BB { - static counter = 0 + static counter = 0 - constructor(@Inject(AA) private readonly aa: AA) { - BBImpl.counter += 1 - } + constructor(@Inject(AA) private readonly aa: AA) { + BBImpl.counter += 1 + } - get key(): string { - return this.aa.key + 'bb' - } + get key(): string { + return this.aa.key + 'bb' + } - public getConstructedTime(): number { - return BBImpl.counter - } + public getConstructedTime(): number { + return BBImpl.counter + } } export const BBFactory: DependencyPair = [ - bbI, - { - useFactory: (aa: AA) => { - return { - key: aa.key + 'bb2', - } - }, - deps: [AA], - }, + bbI, + { + useFactory: (aa: AA) => { + return { + key: aa.key + 'bb2', + } + }, + deps: [AA], + }, ] export const BBLoader: DependencyPair = [ - 'dead', - { - useAsync: () => import('./async.dead').then((module) => module.A), - }, + 'dead', + { + useAsync: () => import('./async.dead').then((module) => module.A), + }, ] export const BBValue: BB = { - key: 'bb3', + key: 'bb3', } diff --git a/test/core.spec.ts b/test/core.spec.ts index ebc2f1d..8305e3f 100644 --- a/test/core.spec.ts +++ b/test/core.spec.ts @@ -4,18 +4,18 @@ import { vi, describe, afterEach, it, expect } from 'vitest' import { - AsyncHook, - createIdentifier, - IDisposable, - forwardRef, - Inject, - Injector, - Many, - Optional, - Self, - setDependencies, - SkipSelf, - WithNew, + AsyncHook, + createIdentifier, + IDisposable, + forwardRef, + Inject, + Injector, + Many, + Optional, + Self, + setDependencies, + SkipSelf, + WithNew, } from '@wendellhu/redi' import { TEST_ONLY_clearKnownIdentifiers } from '../src/decorators' @@ -24,1159 +24,1159 @@ import { AA, BB, bbI } from './async/async.base' import { expectToThrow } from './util/expectToThrow' function cleanupTest() { - TEST_ONLY_clearKnownIdentifiers() + TEST_ONLY_clearKnownIdentifiers() } describe('core', () => { - describe('basics', () => { - afterEach(() => cleanupTest()) + describe('basics', () => { + afterEach(() => cleanupTest()) - it('should throw error when identifier has been declared before', () => { - createIdentifier('a') - - expectToThrow( - () => createIdentifier('a'), - '[redi]: Identifier "a" already exists.' - ) - }) - - it('should resolve instance and then cache it', () => { - let createCount = 0 + it('should throw error when identifier has been declared before', () => { + createIdentifier('a') + + expectToThrow( + () => createIdentifier('a'), + '[redi]: Identifier "a" already exists.' + ) + }) + + it('should resolve instance and then cache it', () => { + let createCount = 0 - class A { - constructor() { - createCount += 1 - } - } + class A { + constructor() { + createCount += 1 + } + } - const j = new Injector([[A]]) + const j = new Injector([[A]]) - j.get(A) - expect(createCount).toBe(1) + j.get(A) + expect(createCount).toBe(1) - j.get(A) - expect(createCount).toBe(1) - }) + j.get(A) + expect(createCount).toBe(1) + }) - it('should support adding dependencies', () => { - const j = new Injector() + it('should support adding dependencies', () => { + const j = new Injector() - class A { - key = 'a' - } + class A { + key = 'a' + } - class B { - constructor(@Inject(A) public a: A) {} - } + class B { + constructor(@Inject(A) public a: A) {} + } - interface C { - key: string - } + interface C { + key: string + } - const cI = createIdentifier('cI') - const cII = createIdentifier('cII') + const cI = createIdentifier('cI') + const cII = createIdentifier('cII') - const a = new A() + const a = new A() - j.add([A, a]) - j.add([B]) - j.add([ - cI, - { - useFactory: (a: A) => ({ - key: a.key, - }), - deps: [A], - }, - ]) - j.add([ - cII, - { - useFactory: (a: A) => ({ - key: a.key, - }), - deps: [A], - }, - ]) + j.add([A, a]) + j.add([B]) + j.add([ + cI, + { + useFactory: (a: A) => ({ + key: a.key, + }), + deps: [A], + }, + ]) + j.add([ + cII, + { + useFactory: (a: A) => ({ + key: a.key, + }), + deps: [A], + }, + ]) - const b = j.get(B) - expect(b.a).toBe(a) - - const c = j.get(cI) - expect(c.key).toBe('a') + const b = j.get(B) + expect(b.a).toBe(a) + + const c = j.get(cI) + expect(c.key).toBe('a') - const cii = j.get(cII) - expect(cii.key).toBe('a') - }) - - it('should throw error when adding a dependency after it get resolved', () => { - const j = new Injector() - - interface IA { - key: string - } - - const IA = createIdentifier('IA') - - class A implements IA { - key = 'a' - } - - class B { - constructor(@Inject(IA) public a: IA) {} - } - - j.add([IA, { useClass: A }]) - j.add([B]) - - j.get(B) - - class AA implements IA { - key = 'aa' - } - - expectToThrow(() => { - j.add([IA, { useClass: AA }]) - }) - }) - - it('should support replacing dependency', () => { - const j = new Injector() - - interface IA { - key: string - } - - const IA = createIdentifier('IA') - - class A implements IA { - key = 'a' - } - - class AA implements IA { - key = 'aa' - } - - class B { - constructor(@Many(IA) public a: IA[]) {} - } - - j.add([IA, { useClass: A }]) - j.add([B]) - - expect(j.get(B).a.length).toBe(1) - }) - - it('should "createInstance" work', () => { - class A { - key = 'a' - } - - class B { - constructor(@Inject(A) public a: A) {} - } - - const j = new Injector([[A]]) - const b = j.createInstance(B) - - expect(b.a.key).toBe('a') - }) - - it('should "createInstance" support custom args', () => { - class A { - key = 'a' - } - - class B { - constructor( - private readonly otherKey: string, - @Inject(A) public readonly a: A - ) {} + const cii = j.get(cII) + expect(cii.key).toBe('a') + }) + + it('should throw error when adding a dependency after it get resolved', () => { + const j = new Injector() + + interface IA { + key: string + } + + const IA = createIdentifier('IA') + + class A implements IA { + key = 'a' + } + + class B { + constructor(@Inject(IA) public a: IA) {} + } + + j.add([IA, { useClass: A }]) + j.add([B]) + + j.get(B) + + class AA implements IA { + key = 'aa' + } + + expectToThrow(() => { + j.add([IA, { useClass: AA }]) + }) + }) + + it('should support replacing dependency', () => { + const j = new Injector() + + interface IA { + key: string + } + + const IA = createIdentifier('IA') + + class A implements IA { + key = 'a' + } + + class AA implements IA { + key = 'aa' + } + + class B { + constructor(@Many(IA) public a: IA[]) {} + } + + j.add([IA, { useClass: A }]) + j.add([B]) + + expect(j.get(B).a.length).toBe(1) + }) + + it('should "createInstance" work', () => { + class A { + key = 'a' + } + + class B { + constructor(@Inject(A) public a: A) {} + } + + const j = new Injector([[A]]) + const b = j.createInstance(B) + + expect(b.a.key).toBe('a') + }) + + it('should "createInstance" support custom args', () => { + class A { + key = 'a' + } + + class B { + constructor( + private readonly otherKey: string, + @Inject(A) public readonly a: A + ) {} - get key() { - return this.otherKey + 'a' - } - } + get key() { + return this.otherKey + 'a' + } + } - const j = new Injector([[A]]) - const b = j.createInstance(B, 'another ') - expect(b.key).toBe('another a') - }) + const j = new Injector([[A]]) + const b = j.createInstance(B, 'another ') + expect(b.key).toBe('another a') + }) - it('should "createInstance" truncate extra custom args', () => {}) + it('should "createInstance" truncate extra custom args', () => {}) - it('should "createInstance" fill unprovided custom args with "undefined"', () => { - class A { - key = 'a' - } + it('should "createInstance" fill unprovided custom args with "undefined"', () => { + class A { + key = 'a' + } - class B { - constructor( - private readonly otherKey: string, - private readonly secondKey: string, - @Inject(A) public readonly a: A - ) {} + class B { + constructor( + private readonly otherKey: string, + private readonly secondKey: string, + @Inject(A) public readonly a: A + ) {} - get key() { - return this.otherKey + this.secondKey + ' ' + this.a.key - } - } + get key() { + return this.otherKey + this.secondKey + ' ' + this.a.key + } + } - const spy = vi.spyOn(console, 'warn') - spy.mockImplementation(() => {}) + const spy = vi.spyOn(console, 'warn') + spy.mockImplementation(() => {}) - const j = new Injector([[A]]) - const b = j.createInstance(B, 'another ') + const j = new Injector([[A]]) + const b = j.createInstance(B, 'another ') - expect(b.key).toBe('another undefined a') - expect(spy).toHaveReturnedTimes(1) - // expect(spy).toHaveBeenCalledWith(`[redi]: Expect 2 custom parameter(s) of class { - // constructor(otherKey, secondKey, a) { - // this.otherKey = otherKey; - // this.secondKey = secondKey; - // this.a = a; - // } - // get key() { - // return this.otherKey + this.secondKey + \" \" + this.a.key; - // } - // } but get 1.`) + expect(b.key).toBe('another undefined a') + expect(spy).toHaveReturnedTimes(1) + // expect(spy).toHaveBeenCalledWith(`[redi]: Expect 2 custom parameter(s) of class { + // constructor(otherKey, secondKey, a) { + // this.otherKey = otherKey; + // this.secondKey = secondKey; + // this.a = a; + // } + // get key() { + // return this.otherKey + this.secondKey + \" \" + this.a.key; + // } + // } but get 1.`) - spy.mockRestore() - }) + spy.mockRestore() + }) - it('should detect circular dependency', () => { - const aI = createIdentifier('aI') - const bI = createIdentifier('bI') + it('should detect circular dependency', () => { + const aI = createIdentifier('aI') + const bI = createIdentifier('bI') - class A { - constructor(@Inject(bI) private readonly b: any) {} - } + class A { + constructor(@Inject(bI) private readonly b: any) {} + } - class B { - constructor(@Inject(aI) private readonly a: any) {} - } + class B { + constructor(@Inject(aI) private readonly a: any) {} + } - const j = new Injector([ - [aI, { useClass: A }], - [bI, { useClass: B }], - ]) + const j = new Injector([ + [aI, { useClass: A }], + [bI, { useClass: B }], + ]) - expectToThrow( - () => j.get(aI), - `[redi]: Detecting cyclic dependency. The last identifier is "B".` - ) - }) + expectToThrow( + () => j.get(aI), + `[redi]: Detecting cyclic dependency. The last identifier is "B".` + ) + }) - it('should "invoke" work', () => { - class A { - a = 'a' - } + it('should "invoke" work', () => { + class A { + a = 'a' + } - const j = new Injector([[A]]) + const j = new Injector([[A]]) - const a = j.invoke((accessor) => { - return accessor.get(A).a - }) + const a = j.invoke((accessor) => { + return accessor.get(A).a + }) - expect(a).toBe('a') - }) + expect(a).toBe('a') + }) - it('should support checking if a dependency could be resolved by an injector', () => { - class A {} + it('should support checking if a dependency could be resolved by an injector', () => { + class A {} - class B {} + class B {} - const j = new Injector([[A]]) + const j = new Injector([[A]]) - expect(j.has(A)).toBeTruthy() - expect(j.has(B)).toBeFalsy() - }) - }) + expect(j.has(A)).toBeTruthy() + expect(j.has(B)).toBeFalsy() + }) + }) - describe('different types of dependency items', () => { - describe('class item', () => { - afterEach(() => cleanupTest()) + describe('different types of dependency items', () => { + describe('class item', () => { + afterEach(() => cleanupTest()) - it('should dispose idle callback when dependency immediately resolved', async () => { - interface A { - key: string - } + it('should dispose idle callback when dependency immediately resolved', async () => { + interface A { + key: string + } - let count = 0 + let count = 0 - const aI = createIdentifier('aI') + const aI = createIdentifier('aI') - class A1 implements A { - key = 'a' + class A1 implements A { + key = 'a' - constructor() { - count += 1 - } - } + constructor() { + count += 1 + } + } - class B { - key: string + class B { + key: string - constructor(@aI private readonly _a: A) { - this.key = this._a.key + 'b' - } - } + constructor(@aI private readonly _a: A) { + this.key = this._a.key + 'b' + } + } - const j = new Injector([[B], [aI, { useClass: A1, lazy: true }]]) + const j = new Injector([[B], [aI, { useClass: A1, lazy: true }]]) - j.get(B) - expect(count).toBe(1) + j.get(B) + expect(count).toBe(1) - await new Promise((resolve) => setTimeout(resolve, 200)) + await new Promise((resolve) => setTimeout(resolve, 200)) - expect(count).toBe(1) - }) + expect(count).toBe(1) + }) - it('should initialize when lazy class instance is actually accessed', () => { - interface A { - key: string + it('should initialize when lazy class instance is actually accessed', () => { + interface A { + key: string - getAnotherKey(): string - } + getAnotherKey(): string + } - let flag = false + let flag = false - const aI = createIdentifier('aI') + const aI = createIdentifier('aI') - class A1 implements A { - key = 'a' + class A1 implements A { + key = 'a' - constructor() { - flag = true - } + constructor() { + flag = true + } - getAnotherKey(): string { - return 'another ' + this.key - } - } + getAnotherKey(): string { + return 'another ' + this.key + } + } - class B { - constructor(@Inject(aI) private a: A) {} + class B { + constructor(@Inject(aI) private a: A) {} - get key(): string { - return this.a.key + 'b' - } + get key(): string { + return this.a.key + 'b' + } - getAnotherKey(): string { - return this.a.getAnotherKey() + 'b' - } + getAnotherKey(): string { + return this.a.getAnotherKey() + 'b' + } - setKey(): void { - this.a.key = 'changed ' - } - } + setKey(): void { + this.a.key = 'changed ' + } + } - const j = new Injector([[B], [aI, { useClass: A1, lazy: true }]]) + const j = new Injector([[B], [aI, { useClass: A1, lazy: true }]]) - const b = j.get(B) - expect(flag).toBeFalsy() + const b = j.get(B) + expect(flag).toBeFalsy() - expect(b.key).toBe('ab') - expect(flag).toBeTruthy() + expect(b.key).toBe('ab') + expect(flag).toBeTruthy() - expect(b.getAnotherKey()).toBe('another ab') + expect(b.getAnotherKey()).toBe('another ab') - b.setKey() + b.setKey() - expect(b.getAnotherKey()).toBe('another changed b') - }) + expect(b.getAnotherKey()).toBe('another changed b') + }) - it('should support "setDependencies"', () => { - class A { - key = 'a' - } + it('should support "setDependencies"', () => { + class A { + key = 'a' + } - class B { - constructor(public readonly a: A) {} - } + class B { + constructor(public readonly a: A) {} + } - setDependencies(B, [[A]]) + setDependencies(B, [[A]]) - const j = new Injector([[A], [B]]) + const j = new Injector([[A], [B]]) - const b = j.get(B) + const b = j.get(B) - expect(b.a.key).toBe('a') - }) + expect(b.a.key).toBe('a') + }) - it('should warn use when a dependency is missing', () => { - class A { - constructor(private b: typeof B) {} + it('should warn use when a dependency is missing', () => { + class A { + constructor(private b: typeof B) {} - get key(): string { - return typeof this.b === 'undefined' - ? 'undefined' - : 'a' + this.b.key - } - } + get key(): string { + return typeof this.b === 'undefined' + ? 'undefined' + : 'a' + this.b.key + } + } - // mock that B is not assigned to the class constructor - let B: any = undefined + // mock that B is not assigned to the class constructor + let B: any = undefined - expectToThrow(() => { - setDependencies(A, [[B]]) - }, '[redi]: It seems that you register "undefined" as dependency on the 1 parameter of "A".') + expectToThrow(() => { + setDependencies(A, [[B]]) + }, '[redi]: It seems that you register "undefined" as dependency on the 1 parameter of "A".') - B = class { - key = 'b' - } - }) + B = class { + key = 'b' + } + }) - it('[class item] should throw error when a dependency cannot be resolved', () => { - class A {} + it('[class item] should throw error when a dependency cannot be resolved', () => { + class A {} - class B { - constructor(_param: string, @Inject(A) private readonly _a: A) {} - } + class B { + constructor(_param: string, @Inject(A) private readonly _a: A) {} + } - const j = new Injector([[B]]) - expectToThrow(() => { - j.get(B) - }, '[redi]: Cannot find "A" registered by any injector. It is the 1th param of "B".') - }) - }) - - describe('instance item', () => { - afterEach(() => cleanupTest()) - - it('should just work', () => { - const a = { - key: 'a', - } - - interface A { - key: string - } - - const aI = createIdentifier('aI') - - const j = new Injector([[aI, { useValue: a }]]) - - expect(j.get(aI).key).toBe('a') - }) - }) - - describe('factory item', () => { - afterEach(() => cleanupTest()) - - it('should just work with zero dep', () => { - interface A { - key: string - } - - const aI = createIdentifier('aI') - - const j = new Injector([ - [ - aI, - { - useFactory: () => ({ - key: 'a', - }), - }, - ], - ]) - - expect(j.get(aI).key).toBe('a') - }) - - it('[factory item] should throw error when a dependency cannot be resolved', () => { - class A {} - - interface IB { - name: string - } - - const b = createIdentifier('b') - - const j = new Injector([ - [b, { useFactory: (_a: A) => ({ name: b }), deps: [A] }], - ]) - expectToThrow(() => { - j.get(b) - }, '[redi]: Cannot find "A" registered by any injector. It is the 0th param of "b".') - }) - }) - - describe('async item', () => { - afterEach(() => cleanupTest()) - - it('should support async loaded ctor', () => - new Promise((done) => { - const j = new Injector([ - [AA], - [ - bbI, - { - useAsync: () => - import('./async/async.item').then((module) => module.BBImpl), - }, - ], - ]) - - j.getAsync(bbI).then((bb) => { - expect(bb.key).toBe('aabb') - expect(bb.getConstructedTime?.()).toBe(1) - }) - - // should check if instantiated in whenReady - j.getAsync(bbI).then((bb) => { - expect(bb.key).toBe('aabb') - expect(bb.getConstructedTime?.()).toBe(1) - }) - - new Promise((resolve) => setTimeout(resolve, 3000)).then(() => { - // should use cached value this time - j.getAsync(bbI).then((bb) => { - expect(bb.key).toBe('aabb') - expect(bb.getConstructedTime?.()).toBe(1) - done() - }) - }) - })) - - it('should support async loaded factory', () => - new Promise((done) => { - const j = new Injector([ - [AA], - [ - bbI, - { - useAsync: () => - import('./async/async.item').then( - (module) => module.BBFactory - ), - }, - ], - ]) - - j.getAsync(bbI).then((bb) => { - expect(bb.key).toBe('aabb2') - done() - }) - })) - - it('should support async loaded value', () => - new Promise((done) => { - const j = new Injector([ - [AA], - [ - bbI, - { - useAsync: () => - import('./async/async.item').then((module) => module.BBValue), - }, - ], - ]) - - j.getAsync(bbI).then((bb) => { - expect(bb.key).toBe('bb3') - done() - }) - })) - - it('should "getAsync" support sync dependency items', () => - new Promise((done) => { - interface A { - key: string - } - - const iA = createIdentifier('iA') - - const j = new Injector([ - [ - iA, - { - useValue: { - key: 'a', - }, - }, - ], - ]) - - j.getAsync(iA).then((a) => { - expect(a.key).toBe('a') - done() - }) - })) - - it('should throw error when async loader returns a async loader', () => - new Promise((done) => { - const j = new Injector([ - [AA], - [ - bbI, - { - useAsync: () => - import('./async/async.item').then( - (module) => module.BBLoader - ), - }, - ], - ]) - - j.getAsync(bbI) - .then((bb) => { - expect(bb.key).toBe('aabb2') - }) - .catch(() => { - // the test would end up here - done() - }) - })) - - it('should throw error when get an async item via "get"', () => { - const j = new Injector([ - [AA], - [ - bbI, - { - useAsync: () => - import('./async/async.item').then((module) => module.BBFactory), - }, - ], - ]) - - expectToThrow(() => { - j.get(bbI) - }, '[redi]: Cannot get async item "bb" from sync api.') - }) - - it('should "AsyncHook" work', () => - new Promise((done) => { - class A { - constructor(@Inject(bbI) private bbILoader: AsyncHook) {} - - public readKey(): Promise { - return this.bbILoader.whenReady().then((bb) => bb.key) - } - } - - const j = new Injector([ - [A], - [AA], - [ - bbI, - { - useAsync: () => - import('./async/async.item').then( - (module) => module.BBFactory - ), - }, - ], - ]) - - j.get(A) - .readKey() - .then((key) => { - expect(key).toBe('aabb2') - done() - }) - .catch(() => { - expect(false).toBeTruthy() // intent to make this test fail - done() - }) - })) - }) - - describe('injector', () => { - afterEach(() => cleanupTest()) - - it('should support inject itself', () => { - const a = { - key: 'a', - } - - interface A { - key: string - } - - const aI = createIdentifier('aI') - - const j = new Injector([[aI, { useValue: a }]]) - - // totally verbose for real use case, but to show this works - expect(j.get(Injector).get(aI).key).toBe('a') - }) - }) - }) - - describe('quantities', () => { - afterEach(() => cleanupTest()) - - it('should support "Many"', () => { - interface A { - key: string - } - - class A1 implements A { - key = 'a1' - } - - class A2 implements A { - key = 'a2' - } - - const aI = createIdentifier('aI') - - class B { - constructor(@Many(aI) private aS: A[]) {} - - get key(): string { - return this.aS.map((a) => a.key).join('') + 'b' - } - } - - const cI = createIdentifier('cI') - - const j = new Injector([ - [aI, { useClass: A1 }], - [aI, { useClass: A2 }], - [B], - [ - cI, - { - useFactory: (aS: A[]) => ({ - key: aS.map((a) => a.key).join('') + 'c', - }), - deps: [[new Many(), aI]], - }, - ], - ]) - - expect(j.get(B).key).toBe('a1a2b') - expect(j.get(cI).key).toBe('a1a2c') - }) - - it('should support "Optional"', () => { - interface A { - key: string - } - - const aI = createIdentifier('aI') - - class B { - constructor(@Optional() @aI private a?: A) {} - - get key(): string { - return this.a?.key || 'no a' + 'b' - } - } - - const cI = createIdentifier('cI') - - const j = new Injector([ - [B], - [ - cI, - { - useFactory: (aS?: A) => ({ - key: aS?.key || 'no a' + 'c', - }), - deps: [[new Optional(), aI]], - }, - ], - ]) - - expect(j.get(B).key).toBe('no ab') - expect(j.get(cI).key).toBe('no ac') - }) - - it('should throw error when using decorator on a non-injectable parameter', () => { - class A {} - - expectToThrow(() => { - class B { - constructor(@Optional() _a: A) {} - } - }, `[redi]: Could not find dependency registered on the 0 (indexed) parameter of the constructor of "B".`) - }) - - it('should throw error when a required / optional dependency is provided with many values', () => { - interface A { - key: string - } - - const aI = createIdentifier('aI') - - class B { - constructor(@aI private a: A) {} - - get key(): string { - return this.a?.key || 'no a' + 'b' - } - } - - const j = new Injector([ - [B], - [aI, { useValue: { key: 'a1' } }], - [aI, { useValue: { key: 'a2' } }], - ]) - - expectToThrow( - () => j.get(B), - `[redi]: Expect "required" dependency items for id "aI" but get 2.` - ) - }) - }) - - describe('layered injection system', () => { - afterEach(() => cleanupTest()) - - it('should get dependencies upwards', () => { - class A { - key = 'a' - } - - const cI = createIdentifier('cI') - - interface C { - key: string - } - - class B { - constructor(@Inject(A) private a: A, @Inject(cI) private c: C) {} - - get key() { - return this.a.key + 'b' + this.c.key - } - } - - class C1 implements C { - key = 'c1' - } - - class C2 implements C { - key = 'c2' - } - - const injector = new Injector([[A], [B], [cI, { useClass: C1 }]]) - const child = injector.createChild([[cI, { useClass: C2 }]]) - - const b = child.get(B) - expect(b.key).toBe('abc1') - }) - - it('should work with "SkipSelf"', () => { - class A { - key = 'a' - } - - interface B { - key: string - } - - const bI = createIdentifier('bI') - const cI = createIdentifier('cI') - - interface C { - key: string - } - - class C1 implements C { - key = 'c1' - } - - class C2 implements C { - key = 'c2' - } - - class D { - constructor(@SkipSelf() @cI private readonly c: C) {} - - get key(): string { - return this.c.key + 'd' - } - } - - const injector = new Injector([[A], [cI, { useClass: C1 }]]) - const child = injector.createChild([ - [cI, { useClass: C2 }], - [ - bI, - { - useFactory: (a: A, c: C) => ({ - key: a.key + 'b' + c.key, - }), - deps: [A, [new SkipSelf(), cI]], - }, - ], - [D], - ]) - - const b = child.get(bI) - expect(b.key).toBe('abc1') - - const d = child.get(D) - expect(d.key).toBe('c1d') - }) - - it('should throw error if could not resolve with "Self"', () => { - class A { - key = 'a' - } - - interface B { - key: string - } - - const bI = createIdentifier('bI') - const cI = createIdentifier('cI') - - interface C { - key: string - } - - class C1 implements C { - key = 'c1' - } - - const injector = new Injector([[A], [cI, { useClass: C1 }]]) - const child = injector.createChild([ - [ - bI, - { - useFactory: (a: A, c: C) => ({ - key: a.key + 'b' + c.key, - }), - deps: [A, [new Self(), cI]], - }, - ], - ]) - - expectToThrow( - () => child.get(bI), - '[redi]: Cannot find "cI" registered by any injector.' - ) - }) - - it('should throw error when no ancestor injector could provide dependency', () => { - class A {} - - const j = new Injector() - - expectToThrow( - () => j.get(A), - `[redi]: Cannot find "A" registered by any injector.` - ) - }) - }) - - describe('forwardRef', () => { - afterEach(() => cleanupTest()) - - it('should throw Error when forwardRef is not used', () => { - expectToThrow(() => { - class A { - constructor(@Inject(B) private b: B) {} - - get key(): string { - return typeof this.b === 'undefined' - ? 'undefined' - : 'a' + this.b.key - } - } - - class B { - key = 'b' - } - }, `Cannot access 'B' before initialization`) - }) - - it('should work when "forwardRef" is used', () => { - class A { - constructor(@Inject(forwardRef(() => B)) private b: B) {} - - get key(): string { - return typeof this.b === 'undefined' ? 'undefined' : 'a' + this.b.key - } - } - - class B { - key = 'b' - } - - const j = new Injector([[A], [B]]) - expect(j.get(A).key).toBe('ab') - }) - }) - - describe('non singleton', () => { - it('should work with "WithNew" - classes', () => { - let c = 0 - - class A { - count = c++ - } - - class B { - constructor(@WithNew() @Inject(A) private readonly a: A) {} - - get(): number { - return this.a.count - } - } - - const j = new Injector([[A], [B]]) - const b1 = j.createInstance(B) - const b2 = j.createInstance(B) - - expect(b1.get()).toBe(0) - expect(b2.get()).toBe(1) - }) - - it('should work with "WithNew" - factories', () => { - let c = 0 - - const ICount = createIdentifier('ICount') - - class B { - constructor(@WithNew() @Inject(ICount) public readonly count: number) {} - } - - const j = new Injector([[B], [ICount, { useFactory: () => c++ }]]) - - const b1 = j.createInstance(B) - const b2 = j.createInstance(B) - - expect(b1.count).toBe(0) - expect(b2.count).toBe(1) - }) - }) - - describe('hooks', () => { - afterEach(() => cleanupTest()) - - it('should "onInstantiation" work for class dependencies', () => { - interface A { - key: string - - getAnotherKey(): string - } - - let flag = false - - const aI = createIdentifier('aI') - - class A1 implements A { - key = 'a' + const j = new Injector([[B]]) + expectToThrow(() => { + j.get(B) + }, '[redi]: Cannot find "A" registered by any injector. It is the 1th param of "B".') + }) + }) + + describe('instance item', () => { + afterEach(() => cleanupTest()) + + it('should just work', () => { + const a = { + key: 'a', + } + + interface A { + key: string + } + + const aI = createIdentifier('aI') + + const j = new Injector([[aI, { useValue: a }]]) + + expect(j.get(aI).key).toBe('a') + }) + }) + + describe('factory item', () => { + afterEach(() => cleanupTest()) + + it('should just work with zero dep', () => { + interface A { + key: string + } + + const aI = createIdentifier('aI') + + const j = new Injector([ + [ + aI, + { + useFactory: () => ({ + key: 'a', + }), + }, + ], + ]) + + expect(j.get(aI).key).toBe('a') + }) + + it('[factory item] should throw error when a dependency cannot be resolved', () => { + class A {} + + interface IB { + name: string + } + + const b = createIdentifier('b') + + const j = new Injector([ + [b, { useFactory: (_a: A) => ({ name: b }), deps: [A] }], + ]) + expectToThrow(() => { + j.get(b) + }, '[redi]: Cannot find "A" registered by any injector. It is the 0th param of "b".') + }) + }) + + describe('async item', () => { + afterEach(() => cleanupTest()) + + it('should support async loaded ctor', () => + new Promise((done) => { + const j = new Injector([ + [AA], + [ + bbI, + { + useAsync: () => + import('./async/async.item').then((module) => module.BBImpl), + }, + ], + ]) + + j.getAsync(bbI).then((bb) => { + expect(bb.key).toBe('aabb') + expect(bb.getConstructedTime?.()).toBe(1) + }) + + // should check if instantiated in whenReady + j.getAsync(bbI).then((bb) => { + expect(bb.key).toBe('aabb') + expect(bb.getConstructedTime?.()).toBe(1) + }) + + new Promise((resolve) => setTimeout(resolve, 3000)).then(() => { + // should use cached value this time + j.getAsync(bbI).then((bb) => { + expect(bb.key).toBe('aabb') + expect(bb.getConstructedTime?.()).toBe(1) + done() + }) + }) + })) + + it('should support async loaded factory', () => + new Promise((done) => { + const j = new Injector([ + [AA], + [ + bbI, + { + useAsync: () => + import('./async/async.item').then( + (module) => module.BBFactory + ), + }, + ], + ]) + + j.getAsync(bbI).then((bb) => { + expect(bb.key).toBe('aabb2') + done() + }) + })) + + it('should support async loaded value', () => + new Promise((done) => { + const j = new Injector([ + [AA], + [ + bbI, + { + useAsync: () => + import('./async/async.item').then((module) => module.BBValue), + }, + ], + ]) + + j.getAsync(bbI).then((bb) => { + expect(bb.key).toBe('bb3') + done() + }) + })) + + it('should "getAsync" support sync dependency items', () => + new Promise((done) => { + interface A { + key: string + } + + const iA = createIdentifier('iA') + + const j = new Injector([ + [ + iA, + { + useValue: { + key: 'a', + }, + }, + ], + ]) + + j.getAsync(iA).then((a) => { + expect(a.key).toBe('a') + done() + }) + })) + + it('should throw error when async loader returns a async loader', () => + new Promise((done) => { + const j = new Injector([ + [AA], + [ + bbI, + { + useAsync: () => + import('./async/async.item').then( + (module) => module.BBLoader + ), + }, + ], + ]) + + j.getAsync(bbI) + .then((bb) => { + expect(bb.key).toBe('aabb2') + }) + .catch(() => { + // the test would end up here + done() + }) + })) + + it('should throw error when get an async item via "get"', () => { + const j = new Injector([ + [AA], + [ + bbI, + { + useAsync: () => + import('./async/async.item').then((module) => module.BBFactory), + }, + ], + ]) + + expectToThrow(() => { + j.get(bbI) + }, '[redi]: Cannot get async item "bb" from sync api.') + }) + + it('should "AsyncHook" work', () => + new Promise((done) => { + class A { + constructor(@Inject(bbI) private bbILoader: AsyncHook) {} + + public readKey(): Promise { + return this.bbILoader.whenReady().then((bb) => bb.key) + } + } + + const j = new Injector([ + [A], + [AA], + [ + bbI, + { + useAsync: () => + import('./async/async.item').then( + (module) => module.BBFactory + ), + }, + ], + ]) + + j.get(A) + .readKey() + .then((key) => { + expect(key).toBe('aabb2') + done() + }) + .catch(() => { + expect(false).toBeTruthy() // intent to make this test fail + done() + }) + })) + }) + + describe('injector', () => { + afterEach(() => cleanupTest()) + + it('should support inject itself', () => { + const a = { + key: 'a', + } + + interface A { + key: string + } + + const aI = createIdentifier('aI') + + const j = new Injector([[aI, { useValue: a }]]) + + // totally verbose for real use case, but to show this works + expect(j.get(Injector).get(aI).key).toBe('a') + }) + }) + }) + + describe('quantities', () => { + afterEach(() => cleanupTest()) + + it('should support "Many"', () => { + interface A { + key: string + } + + class A1 implements A { + key = 'a1' + } + + class A2 implements A { + key = 'a2' + } + + const aI = createIdentifier('aI') + + class B { + constructor(@Many(aI) private aS: A[]) {} + + get key(): string { + return this.aS.map((a) => a.key).join('') + 'b' + } + } + + const cI = createIdentifier('cI') + + const j = new Injector([ + [aI, { useClass: A1 }], + [aI, { useClass: A2 }], + [B], + [ + cI, + { + useFactory: (aS: A[]) => ({ + key: aS.map((a) => a.key).join('') + 'c', + }), + deps: [[new Many(), aI]], + }, + ], + ]) + + expect(j.get(B).key).toBe('a1a2b') + expect(j.get(cI).key).toBe('a1a2c') + }) + + it('should support "Optional"', () => { + interface A { + key: string + } + + const aI = createIdentifier('aI') + + class B { + constructor(@Optional() @aI private a?: A) {} + + get key(): string { + return this.a?.key || 'no a' + 'b' + } + } + + const cI = createIdentifier('cI') + + const j = new Injector([ + [B], + [ + cI, + { + useFactory: (aS?: A) => ({ + key: aS?.key || 'no a' + 'c', + }), + deps: [[new Optional(), aI]], + }, + ], + ]) + + expect(j.get(B).key).toBe('no ab') + expect(j.get(cI).key).toBe('no ac') + }) + + it('should throw error when using decorator on a non-injectable parameter', () => { + class A {} + + expectToThrow(() => { + class B { + constructor(@Optional() _a: A) {} + } + }, `[redi]: Could not find dependency registered on the 0 (indexed) parameter of the constructor of "B".`) + }) + + it('should throw error when a required / optional dependency is provided with many values', () => { + interface A { + key: string + } + + const aI = createIdentifier('aI') + + class B { + constructor(@aI private a: A) {} + + get key(): string { + return this.a?.key || 'no a' + 'b' + } + } + + const j = new Injector([ + [B], + [aI, { useValue: { key: 'a1' } }], + [aI, { useValue: { key: 'a2' } }], + ]) + + expectToThrow( + () => j.get(B), + `[redi]: Expect "required" dependency items for id "aI" but get 2.` + ) + }) + }) + + describe('layered injection system', () => { + afterEach(() => cleanupTest()) + + it('should get dependencies upwards', () => { + class A { + key = 'a' + } + + const cI = createIdentifier('cI') + + interface C { + key: string + } + + class B { + constructor(@Inject(A) private a: A, @Inject(cI) private c: C) {} + + get key() { + return this.a.key + 'b' + this.c.key + } + } + + class C1 implements C { + key = 'c1' + } + + class C2 implements C { + key = 'c2' + } + + const injector = new Injector([[A], [B], [cI, { useClass: C1 }]]) + const child = injector.createChild([[cI, { useClass: C2 }]]) + + const b = child.get(B) + expect(b.key).toBe('abc1') + }) + + it('should work with "SkipSelf"', () => { + class A { + key = 'a' + } + + interface B { + key: string + } + + const bI = createIdentifier('bI') + const cI = createIdentifier('cI') + + interface C { + key: string + } + + class C1 implements C { + key = 'c1' + } + + class C2 implements C { + key = 'c2' + } + + class D { + constructor(@SkipSelf() @cI private readonly c: C) {} + + get key(): string { + return this.c.key + 'd' + } + } + + const injector = new Injector([[A], [cI, { useClass: C1 }]]) + const child = injector.createChild([ + [cI, { useClass: C2 }], + [ + bI, + { + useFactory: (a: A, c: C) => ({ + key: a.key + 'b' + c.key, + }), + deps: [A, [new SkipSelf(), cI]], + }, + ], + [D], + ]) + + const b = child.get(bI) + expect(b.key).toBe('abc1') + + const d = child.get(D) + expect(d.key).toBe('c1d') + }) + + it('should throw error if could not resolve with "Self"', () => { + class A { + key = 'a' + } + + interface B { + key: string + } + + const bI = createIdentifier('bI') + const cI = createIdentifier('cI') + + interface C { + key: string + } + + class C1 implements C { + key = 'c1' + } + + const injector = new Injector([[A], [cI, { useClass: C1 }]]) + const child = injector.createChild([ + [ + bI, + { + useFactory: (a: A, c: C) => ({ + key: a.key + 'b' + c.key, + }), + deps: [A, [new Self(), cI]], + }, + ], + ]) + + expectToThrow( + () => child.get(bI), + '[redi]: Cannot find "cI" registered by any injector.' + ) + }) + + it('should throw error when no ancestor injector could provide dependency', () => { + class A {} + + const j = new Injector() + + expectToThrow( + () => j.get(A), + `[redi]: Cannot find "A" registered by any injector.` + ) + }) + }) + + describe('forwardRef', () => { + afterEach(() => cleanupTest()) + + it('should throw Error when forwardRef is not used', () => { + expectToThrow(() => { + class A { + constructor(@Inject(B) private b: B) {} + + get key(): string { + return typeof this.b === 'undefined' + ? 'undefined' + : 'a' + this.b.key + } + } + + class B { + key = 'b' + } + }, `Cannot access 'B' before initialization`) + }) + + it('should work when "forwardRef" is used', () => { + class A { + constructor(@Inject(forwardRef(() => B)) private b: B) {} + + get key(): string { + return typeof this.b === 'undefined' ? 'undefined' : 'a' + this.b.key + } + } + + class B { + key = 'b' + } + + const j = new Injector([[A], [B]]) + expect(j.get(A).key).toBe('ab') + }) + }) + + describe('non singleton', () => { + it('should work with "WithNew" - classes', () => { + let c = 0 + + class A { + count = c++ + } + + class B { + constructor(@WithNew() @Inject(A) private readonly a: A) {} + + get(): number { + return this.a.count + } + } + + const j = new Injector([[A], [B]]) + const b1 = j.createInstance(B) + const b2 = j.createInstance(B) + + expect(b1.get()).toBe(0) + expect(b2.get()).toBe(1) + }) + + it('should work with "WithNew" - factories', () => { + let c = 0 + + const ICount = createIdentifier('ICount') + + class B { + constructor(@WithNew() @Inject(ICount) public readonly count: number) {} + } + + const j = new Injector([[B], [ICount, { useFactory: () => c++ }]]) + + const b1 = j.createInstance(B) + const b2 = j.createInstance(B) + + expect(b1.count).toBe(0) + expect(b2.count).toBe(1) + }) + }) + + describe('hooks', () => { + afterEach(() => cleanupTest()) + + it('should "onInstantiation" work for class dependencies', () => { + interface A { + key: string + + getAnotherKey(): string + } + + let flag = false + + const aI = createIdentifier('aI') + + class A1 implements A { + key = 'a' - constructor() { - flag = true - } + constructor() { + flag = true + } - getAnotherKey(): string { - return 'another ' + this.key - } - } + getAnotherKey(): string { + return 'another ' + this.key + } + } - class B { - constructor(@Inject(aI) private a: A) {} + class B { + constructor(@Inject(aI) private a: A) {} - get key(): string { - return this.a.key + 'b' - } + get key(): string { + return this.a.key + 'b' + } - getAnotherKey(): string { - return this.a.getAnotherKey() + 'b' - } + getAnotherKey(): string { + return this.a.getAnotherKey() + 'b' + } - setKey(): void { - this.a.key = 'changed ' - } - } + setKey(): void { + this.a.key = 'changed ' + } + } - const j = new Injector([ - [B], - [ - aI, - { - useClass: A1, - lazy: true, - onInstantiation: (i: A) => (i.key = 'a++'), - }, - ], - ]) + const j = new Injector([ + [B], + [ + aI, + { + useClass: A1, + lazy: true, + onInstantiation: (i: A) => (i.key = 'a++'), + }, + ], + ]) - const b = j.get(B) - expect(flag).toBeFalsy() + const b = j.get(B) + expect(flag).toBeFalsy() - expect(b.key).toBe('a++b') - expect(flag).toBeTruthy() + expect(b.key).toBe('a++b') + expect(flag).toBeTruthy() - expect(b.getAnotherKey()).toBe('another a++b') + expect(b.getAnotherKey()).toBe('another a++b') - b.setKey() + b.setKey() - expect(b.getAnotherKey()).toBe('another changed b') - }) + expect(b.getAnotherKey()).toBe('another changed b') + }) - it('should "onInstantiation" work for factory dependencies', () => { - interface A { - key: string - } + it('should "onInstantiation" work for factory dependencies', () => { + interface A { + key: string + } - const aI = createIdentifier('aI') + const aI = createIdentifier('aI') - const j = new Injector([ - [ - aI, - { - useFactory: () => ({ - key: 'a', - }), - onInstantiation: (i: A) => (i.key = 'a++'), - }, - ], - ]) + const j = new Injector([ + [ + aI, + { + useFactory: () => ({ + key: 'a', + }), + onInstantiation: (i: A) => (i.key = 'a++'), + }, + ], + ]) - expect(j.get(aI).key).toBe('a++') - }) - }) + expect(j.get(aI).key).toBe('a++') + }) + }) - describe('dispose', () => { - afterEach(() => cleanupTest()) + describe('dispose', () => { + afterEach(() => cleanupTest()) - it('should dispose', () => { - let flag = false + it('should dispose', () => { + let flag = false - // for test coverage - class A { - key = 'a' - } + // for test coverage + class A { + key = 'a' + } - class B implements IDisposable { - constructor(@Inject(A) private readonly a: A) {} + class B implements IDisposable { + constructor(@Inject(A) private readonly a: A) {} - get key(): string { - return this.a.key + 'b' - } + get key(): string { + return this.a.key + 'b' + } - dispose() { - flag = true - } - } + dispose() { + flag = true + } + } - const j = new Injector([[A], [B]]) - j.get(B) + const j = new Injector([[A], [B]]) + j.get(B) - j.dispose() - - expect(flag).toBeTruthy() - }) + j.dispose() + + expect(flag).toBeTruthy() + }) - it('should throw error when called after disposing', () => { - class A {} + it('should throw error when called after disposing', () => { + class A {} - const j = new Injector() - j.dispose() - - expectToThrow(() => j.get(A), '') - }) - }) + const j = new Injector() + j.dispose() + + expectToThrow(() => j.get(A), '') + }) + }) }) diff --git a/test/react.spec.tsx b/test/react.spec.tsx index 40f5622..94f91f4 100644 --- a/test/react.spec.tsx +++ b/test/react.spec.tsx @@ -8,12 +8,12 @@ import React from 'react' import { createIdentifier, IDisposable, Injector } from '@wendellhu/redi' import { - connectInjector, - connectDependencies, - useInjector, - RediContext, - useDependency, - WithDependency, + connectInjector, + connectDependencies, + useInjector, + RediContext, + useDependency, + WithDependency, } from '@wendellhu/redi/react-bindings' import { TEST_ONLY_clearKnownIdentifiers } from '../src/decorators' @@ -21,166 +21,166 @@ import { TEST_ONLY_clearKnownIdentifiers } from '../src/decorators' import { expectToThrow } from './util/expectToThrow' describe('react', () => { - afterEach(() => { - TEST_ONLY_clearKnownIdentifiers() - }) + afterEach(() => { + TEST_ONLY_clearKnownIdentifiers() + }) - it('should "connectInjector" work', () => { - interface A { - key: string - } + it('should "connectInjector" work', () => { + interface A { + key: string + } - const aI = createIdentifier('aI') + const aI = createIdentifier('aI') - const injector = new Injector([[aI, { useValue: { key: 'a' } }]]) + const injector = new Injector([[aI, { useValue: { key: 'a' } }]]) - const App = connectInjector(function AppImpl() { - const j = useInjector() - const a = j.get(aI) + const App = connectInjector(function AppImpl() { + const j = useInjector() + const a = j.get(aI) - return

- }, injector) + return
{a.key}
+ }, injector) - const { container } = render() - expect(container.firstChild!.textContent).toBe('a') - }) + const { container } = render() + expect(container.firstChild!.textContent).toBe('a') + }) - it('should "connectDependencies" work', () => { - interface A { - key: string - } + it('should "connectDependencies" work', () => { + interface A { + key: string + } - const aI = createIdentifier
('aI') + const aI = createIdentifier('aI') - const App = connectDependencies( - function AppImpl() { - const j = useInjector() - const a = j.get(aI) + const App = connectDependencies( + function AppImpl() { + const j = useInjector() + const a = j.get(aI) - return
{a.key}
- }, - [[aI, { useValue: { key: 'a' } }]] - ) + return
{a.key}
+ }, + [[aI, { useValue: { key: 'a' } }]] + ) - const { container } = render() - expect(container.firstChild!.textContent).toBe('a') - }) + const { container } = render() + expect(container.firstChild!.textContent).toBe('a') + }) - it('should "withDependency" work', () => { - interface A { - key: string - } + it('should "withDependency" work', () => { + interface A { + key: string + } - const aI = createIdentifier
('aI') + const aI = createIdentifier('aI') - const injector = new Injector([[aI, { useValue: { key: 'a' } }]]) + const injector = new Injector([[aI, { useValue: { key: 'a' } }]]) - class AppImpl extends React.Component { - static override contextType = RediContext + class AppImpl extends React.Component { + static override contextType = RediContext - @WithDependency(aI) - private readonly a!: A + @WithDependency(aI) + private readonly a!: A - override render() { - return
{this.a.key}
- } - } + override render() { + return
{this.a.key}
+ } + } - const App = connectInjector(AppImpl, injector) + const App = connectInjector(AppImpl, injector) - const { container } = render() - expect(container.firstChild!.textContent).toBe('a') - }) + const { container } = render() + expect(container.firstChild!.textContent).toBe('a') + }) - it('should "useDependency" work', () => { - interface A { - key: string - } + it('should "useDependency" work', () => { + interface A { + key: string + } - const aI = createIdentifier
('aI') + const aI = createIdentifier('aI') - function AppImpl() { - const a = useDependency(aI) - return
{a.key}
- } + function AppImpl() { + const a = useDependency(aI) + return
{a.key}
+ } - const injector = new Injector([[aI, { useValue: { key: 'a' } }]]) - const App = connectInjector(AppImpl, injector) + const injector = new Injector([[aI, { useValue: { key: 'a' } }]]) + const App = connectInjector(AppImpl, injector) - const { container } = render() - expect(container.firstChild!.textContent).toBe('a') - }) + const { container } = render() + expect(container.firstChild!.textContent).toBe('a') + }) - it('should throw error when using "useInjector" outside of "RediContext"', () => { - function App() { - useInjector() + it('should throw error when using "useInjector" outside of "RediContext"', () => { + function App() { + useInjector() - return
a
- } + return
a
+ } - expectToThrow(() => render()) - }) + expectToThrow(() => render()) + }) - it('should throw error when using "WithDependency" outside of "RediContext"', () => { - interface A { - key: string - } + it('should throw error when using "WithDependency" outside of "RediContext"', () => { + interface A { + key: string + } - const aI = createIdentifier
('aI') + const aI = createIdentifier('aI') - const injector = new Injector([[aI, { useValue: { key: 'a' } }]]) + const injector = new Injector([[aI, { useValue: { key: 'a' } }]]) - class AppImpl extends React.Component { - @WithDependency(aI) - private readonly a!: A + class AppImpl extends React.Component { + @WithDependency(aI) + private readonly a!: A - override render() { - return
{this.a.key}
- } - } + override render() { + return
{this.a.key}
+ } + } - const App = connectInjector(AppImpl, injector) + const App = connectInjector(AppImpl, injector) - expectToThrow(() => render()) - }) + expectToThrow(() => render()) + }) - it('should dispose injector when React component unmounts', async () => { - let disposed = false + it('should dispose injector when React component unmounts', async () => { + let disposed = false - class A implements IDisposable { - key = 'a' + class A implements IDisposable { + key = 'a' - public dispose(): void { - disposed = true - } - } + public dispose(): void { + disposed = true + } + } - const Child = connectDependencies( - function ChildImpl() { - const j = useInjector() - const a = j.get(A) - return
{a.key}
- }, - [[A]] - ) + const Child = connectDependencies( + function ChildImpl() { + const j = useInjector() + const a = j.get(A) + return
{a.key}
+ }, + [[A]] + ) - function App() { - const [mounted, setMounted] = React.useState(true) + function App() { + const [mounted, setMounted] = React.useState(true) - return ( -
- - {mounted && } -
- ) - } + return ( +
+ + {mounted && } +
+ ) + } - const { container } = render() - await act(() => { - fireEvent.click(container.firstElementChild!.firstElementChild!) - return new Promise((res) => setTimeout(res, 20)) - }) + const { container } = render() + await act(() => { + fireEvent.click(container.firstElementChild!.firstElementChild!) + return new Promise((res) => setTimeout(res, 20)) + }) - expect(disposed).toBe(true) - }) + expect(disposed).toBe(true) + }) }) diff --git a/test/rx.spec.tsx b/test/rx.spec.tsx index 523fca9..44916b9 100644 --- a/test/rx.spec.tsx +++ b/test/rx.spec.tsx @@ -10,265 +10,265 @@ import { scan, startWith } from 'rxjs/operators' import { IDisposable } from '@wendellhu/redi' import { - useDependency, - useDependencyValue, - useUpdateBinder, - useDependencyContext, - useDependencyContextValue, - connectDependencies, + useDependency, + useDependencyValue, + useUpdateBinder, + useDependencyContext, + useDependencyContextValue, + connectDependencies, } from '@wendellhu/redi/react-bindings' import { TEST_ONLY_clearKnownIdentifiers } from '../src/decorators' import { expectToThrow } from './util/expectToThrow' describe('rx', () => { - afterEach(() => { - TEST_ONLY_clearKnownIdentifiers() - }) - - it('should demo works with RxJS', async () => { - class CounterService { - counter$ = interval(100).pipe( - startWith(0), - scan((acc) => acc + 1) - ) - } - - const App = connectDependencies( - class extends Component { - override render() { - return - } - }, - [[CounterService]] - ) - - function Display() { - const counter = useDependency(CounterService) - const value = useDependencyValue(counter!.counter$, 0) - - return
{value}
- } - - const { container } = render() - expect(container.firstChild!.textContent).toBe('0') - - await act( - () => new Promise((res) => setTimeout(() => res(void 0), 360)) - ) - expect(container.firstChild!.textContent).toBe('3') - }) - - it('should use default value in BehaviorSubject', async () => { - class CounterService implements IDisposable { - public counter$: BehaviorSubject - private number: number - private readonly loop?: number - - constructor() { - this.number = 5 - this.counter$ = new BehaviorSubject(this.number) - this.loop = setInterval(() => { - this.number += 1 - this.counter$.next(this.number) - }, 100) as any as number - } - - dispose(): void { - clearTimeout(this.loop!) - } - } - - const App = connectDependencies( - function () { - return - }, - [[CounterService]] - ) - - function Child() { - const counterService = useDependency(CounterService) - const count = useDependencyValue(counterService.counter$) - - return
{count}
- } - - const { container } = render() - expect(container.firstChild!.textContent).toBe('5') - - await act( - () => new Promise((res) => setTimeout(() => res(void 0), 320)) - ) - expect(container.firstChild!.textContent).toBe('8') - }) - - it('should not trigger unnecessary re-render when handled correctly', async () => { - let childRenderCount = 0 - - class CounterService { - counter$ = interval(100).pipe( - startWith(0), - scan((acc) => acc + 1) - ) - } - - const App = connectDependencies( - function () { - return - }, - [[CounterService]] - ) - - function Parent() { - const counterService = useDependency(CounterService) - const count = useDependencyValue(counterService.counter$, 0) - - return - } - - function Child(props: { count?: number }) { - childRenderCount += 1 - return
{props.count}
- } - - const { container } = render() - expect(container.firstChild!.textContent).toBe('0') - expect(childRenderCount).toBe(1) - - await act( - () => new Promise((res) => setTimeout(() => res(void 0), 360)) - ) - expect(container.firstChild!.textContent).toBe('3') - expect(childRenderCount).toBe(2) - }) - - it('should not trigger unnecessary re-render with useDependencyContext', async () => { - let childRenderCount = 0 - - class CounterService { - counter$ = interval(100).pipe( - startWith(0), - scan((acc) => acc + 1) - ) - } - - const App = connectDependencies( - function () { - return - }, - [[CounterService]] - ) - - function useCounter$() { - return useDependency(CounterService).counter$ - } - - function Parent() { - const counter$ = useCounter$() - const { Provider: CounterProvider } = useDependencyContext(counter$, 0) - - return ( - - - - ) - } - - function Child() { - const counter$ = useCounter$() - const count = useDependencyContextValue(counter$) - - childRenderCount += 1 - - return
{count}
- } - - const { container } = render() - expect(container.firstChild!.textContent).toBe('0') - expect(childRenderCount).toBe(1) - - await act( - () => new Promise((res) => setTimeout(() => res(void 0), 360)) - ) - expect(childRenderCount).toBe(2) - }) - - it('should raise error when no ancestor subscribe an observable value', async () => { - class CounterService { - counter$ = interval(1000).pipe( - startWith(0), - scan((acc) => acc + 1) - ) - } - - const App = connectDependencies( - function App() { - return - }, - [[CounterService]] - ) - - function useCounter$() { - return useDependency(CounterService).counter$ - } - - function Parent() { - return - } - - function Child() { - const counter$ = useCounter$() - const count = useDependencyContextValue(counter$) - - return
{count}
- } - - expectToThrow( - () => render(), - '[redi]: try to read context value but no ancestor component subscribed it.' - ) - }) - - it('should update whenever `useUpdateBinder` emits', async () => { - class CounterService implements IDisposable { - public number = 0 - public updater$ = new Subject() - - private loop?: number - - constructor() { - this.loop = setInterval(() => { - this.number += 1 - this.updater$.next() - }, 100) as any as number - } - - dispose(): void { - clearTimeout(this.loop!) - } - } - - const App = connectDependencies( - function () { - return - }, - [[CounterService]] - ) - - function Child() { - const counterService = useDependency(CounterService) - - useUpdateBinder(counterService.updater$) - - return
{counterService.number}
- } - - const { container } = render() - expect(container.firstChild!.textContent).toBe('0') - - await act( - () => new Promise((res) => setTimeout(() => res(void 0), 310)) - ) - expect(container.firstChild!.textContent).toBe('3') - }) + afterEach(() => { + TEST_ONLY_clearKnownIdentifiers() + }) + + it('should demo works with RxJS', async () => { + class CounterService { + counter$ = interval(100).pipe( + startWith(0), + scan((acc) => acc + 1) + ) + } + + const App = connectDependencies( + class extends Component { + override render() { + return + } + }, + [[CounterService]] + ) + + function Display() { + const counter = useDependency(CounterService) + const value = useDependencyValue(counter!.counter$, 0) + + return
{value}
+ } + + const { container } = render() + expect(container.firstChild!.textContent).toBe('0') + + await act( + () => new Promise((res) => setTimeout(() => res(void 0), 360)) + ) + expect(container.firstChild!.textContent).toBe('3') + }) + + it('should use default value in BehaviorSubject', async () => { + class CounterService implements IDisposable { + public counter$: BehaviorSubject + private number: number + private readonly loop?: number + + constructor() { + this.number = 5 + this.counter$ = new BehaviorSubject(this.number) + this.loop = setInterval(() => { + this.number += 1 + this.counter$.next(this.number) + }, 100) as any as number + } + + dispose(): void { + clearTimeout(this.loop!) + } + } + + const App = connectDependencies( + function () { + return + }, + [[CounterService]] + ) + + function Child() { + const counterService = useDependency(CounterService) + const count = useDependencyValue(counterService.counter$) + + return
{count}
+ } + + const { container } = render() + expect(container.firstChild!.textContent).toBe('5') + + await act( + () => new Promise((res) => setTimeout(() => res(void 0), 320)) + ) + expect(container.firstChild!.textContent).toBe('8') + }) + + it('should not trigger unnecessary re-render when handled correctly', async () => { + let childRenderCount = 0 + + class CounterService { + counter$ = interval(100).pipe( + startWith(0), + scan((acc) => acc + 1) + ) + } + + const App = connectDependencies( + function () { + return + }, + [[CounterService]] + ) + + function Parent() { + const counterService = useDependency(CounterService) + const count = useDependencyValue(counterService.counter$, 0) + + return + } + + function Child(props: { count?: number }) { + childRenderCount += 1 + return
{props.count}
+ } + + const { container } = render() + expect(container.firstChild!.textContent).toBe('0') + expect(childRenderCount).toBe(1) + + await act( + () => new Promise((res) => setTimeout(() => res(void 0), 360)) + ) + expect(container.firstChild!.textContent).toBe('3') + expect(childRenderCount).toBe(2) + }) + + it('should not trigger unnecessary re-render with useDependencyContext', async () => { + let childRenderCount = 0 + + class CounterService { + counter$ = interval(100).pipe( + startWith(0), + scan((acc) => acc + 1) + ) + } + + const App = connectDependencies( + function () { + return + }, + [[CounterService]] + ) + + function useCounter$() { + return useDependency(CounterService).counter$ + } + + function Parent() { + const counter$ = useCounter$() + const { Provider: CounterProvider } = useDependencyContext(counter$, 0) + + return ( + + + + ) + } + + function Child() { + const counter$ = useCounter$() + const count = useDependencyContextValue(counter$) + + childRenderCount += 1 + + return
{count}
+ } + + const { container } = render() + expect(container.firstChild!.textContent).toBe('0') + expect(childRenderCount).toBe(1) + + await act( + () => new Promise((res) => setTimeout(() => res(void 0), 360)) + ) + expect(childRenderCount).toBe(2) + }) + + it('should raise error when no ancestor subscribe an observable value', async () => { + class CounterService { + counter$ = interval(1000).pipe( + startWith(0), + scan((acc) => acc + 1) + ) + } + + const App = connectDependencies( + function App() { + return + }, + [[CounterService]] + ) + + function useCounter$() { + return useDependency(CounterService).counter$ + } + + function Parent() { + return + } + + function Child() { + const counter$ = useCounter$() + const count = useDependencyContextValue(counter$) + + return
{count}
+ } + + expectToThrow( + () => render(), + '[redi]: try to read context value but no ancestor component subscribed it.' + ) + }) + + it('should update whenever `useUpdateBinder` emits', async () => { + class CounterService implements IDisposable { + public number = 0 + public updater$ = new Subject() + + private loop?: number + + constructor() { + this.loop = setInterval(() => { + this.number += 1 + this.updater$.next() + }, 100) as any as number + } + + dispose(): void { + clearTimeout(this.loop!) + } + } + + const App = connectDependencies( + function () { + return + }, + [[CounterService]] + ) + + function Child() { + const counterService = useDependency(CounterService) + + useUpdateBinder(counterService.updater$) + + return
{counterService.number}
+ } + + const { container } = render() + expect(container.firstChild!.textContent).toBe('0') + + await act( + () => new Promise((res) => setTimeout(() => res(void 0), 310)) + ) + expect(container.firstChild!.textContent).toBe('3') + }) }) diff --git a/test/util/expectToThrow.ts b/test/util/expectToThrow.ts index d8c8945..28431e6 100644 --- a/test/util/expectToThrow.ts +++ b/test/util/expectToThrow.ts @@ -7,14 +7,14 @@ import { vi, expect } from 'vitest' * @param func Function that you would normally pass to `expect(func).toThrow()` */ export const expectToThrow = (func: () => unknown, error?: string): void => { - // Even though the error is caught, it still gets printed to the console - // so we mock that out to avoid the wall of red text. - const spy = vi.spyOn(console, 'error') - spy.mockImplementation(() => { - // empty - }) + // Even though the error is caught, it still gets printed to the console + // so we mock that out to avoid the wall of red text. + const spy = vi.spyOn(console, 'error') + spy.mockImplementation(() => { + // empty + }) - expect(func).toThrow(error) + expect(func).toThrow(error) - spy.mockRestore() + spy.mockRestore() }