Skip to content

Commit

Permalink
fix(injector): make sure type cache is used when finding matching pro…
Browse files Browse the repository at this point in the history
…vider.

Previously a check was wrongly implemented so that isExtendable got no a Type object and has to infer its type first. This happens without cache per default as the function is designed to operate on Type objects and if non is given it assumes it runs in the Processor (where caching for inferring types is disabled).

This commit also adds new debugging output when computation takes a very long time (>100ms)
  • Loading branch information
marcj committed Oct 24, 2023
1 parent 1eb83ff commit 8c79e4b
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 21 deletions.
6 changes: 4 additions & 2 deletions packages/app/src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,9 @@ export class ConfigurationInvalidError extends CustomError {

let moduleId = 0;

/**
* @reflection never
*/
type PartialDeep<T> = T extends string | number | bigint | boolean | null | undefined | symbol | Date
? T | undefined
// Arrays, Sets and Maps and their readonly counterparts have their items made
Expand Down Expand Up @@ -235,7 +238,7 @@ export type ListenerType = EventListener<any> | ClassType;
/**
* The AppModule is the base class for all modules.
*
* You can use `createModule` to create a new module class or extend from `AppModule` manually.
* You can use `createModule` to create a new module class or extend from `AppModule` manually.
*
* @example
* ```typescript
Expand All @@ -251,7 +254,6 @@ export type ListenerType = EventListener<any> | ClassType;
* this.name = 'myModule';
* }
* }
*
*/
export class AppModule<T extends RootModuleDefinition = {}, C extends ExtractClassType<T['config']> = any> extends InjectorModule<C, AppModule<any>> {
public setupConfigs: ((module: AppModule<any>, config: any) => void)[] = [];
Expand Down
18 changes: 16 additions & 2 deletions packages/injector/src/module.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
import { NormalizedProvider, ProviderWithScope, TagProvider, Token } from './provider.js';
import { arrayRemoveItem, ClassType, getClassName, isClass, isPlainObject, isPrototypeOfBase } from '@deepkit/core';
import { BuildContext, Injector, SetupProviderRegistry } from './injector.js';
import { hasTypeInformation, isExtendable, isType, ReceiveType, reflect, ReflectionKind, reflectOrUndefined, resolveReceiveType, TypeClass, TypeObjectLiteral, visit } from '@deepkit/type';
import {
hasTypeInformation,
isExtendable,
isType,
ReceiveType,
reflect,
ReflectionKind,
reflectOrUndefined,
resolveReceiveType,
TypeClass,
TypeObjectLiteral,
visit
} from '@deepkit/type';

export type ConfigureProvider<T> = { [name in keyof T]: T[name] extends (...args: infer A) => any ? (...args: A) => ConfigureProvider<T> : T[name] };

Expand Down Expand Up @@ -67,7 +79,9 @@ function lookupPreparedProviders(preparedProviders: PreparedProvider[], token: T
if (token.kind === ReflectionKind.function && token.function && token.function === preparedProvider.token) last = preparedProvider;
if (isType(preparedProvider.token) && isExtendable(preparedProvider.token, token)) last = preparedProvider;
if (isClass(preparedProvider.token) && hasTypeInformation(preparedProvider.token) && isExtendable(reflect(preparedProvider.token), token)) last = preparedProvider;
} else if ('string' === typeof token || 'number' === typeof token || 'bigint' === typeof token || 'symbol' === typeof token && isType(preparedProvider.token)) {
} else if (('string' === typeof token || 'number' === typeof token || 'bigint' === typeof token || 'symbol' === typeof token) && isType(preparedProvider.token)) {
// note: important that we check for preparedProvider.token being isType, otherwise isExtendable uses typeInfer (which does not use cache).
// we have to call reflect(preparedProvider.token) otherwise, so that the cache is used.
if (isExtendable(preparedProvider.token, { kind: ReflectionKind.literal, literal: token })) last = preparedProvider;
}
}
Expand Down
49 changes: 32 additions & 17 deletions packages/type/src/reflection/extends.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
isTypeIncluded,
ReflectionKind,
resolveTypeMembers,
stringifyType,
Type,
TypeAny,
TypeInfer,
Expand Down Expand Up @@ -56,11 +57,25 @@ function hasStack(extendStack: StackEntry[], left: Type, right: Type): boolean {
*
* See https://www.typescriptlang.org/docs/handbook/type-compatibility.html#any-unknown-object-void-undefined-null-and-never-assignability
* This algo follows strict mode.
*
* Warning: If you do not pass Type objects, typeInfer() is used which does not use cache (it is designed to be called withing type processor)
*/
export function isExtendable(leftValue: AssignableType, rightValue: AssignableType, extendStack: StackEntry[] = []): boolean {
const start = Date.now();
const right: Type = isType(rightValue) ? rightValue : typeInfer(rightValue);
const left: Type = isType(leftValue) ? leftValue : typeInfer(leftValue);

const valid = _isExtendable(left, right, extendStack);
const took = Date.now() - start;

if (took > 100) {
console.warn('isExtendable took very long', Date.now() - start, 'ms comparing', stringifyType(left), 'and', stringifyType(right));
}

return valid;
}

export function _isExtendable(left: Type, right: Type, extendStack: StackEntry[] = []): boolean {
if (hasStack(extendStack, left, right)) return true;

try {
Expand All @@ -79,12 +94,12 @@ export function isExtendable(leftValue: AssignableType, rightValue: AssignableTy
}

if (right.kind === ReflectionKind.any || right.kind === ReflectionKind.unknown) return true;
if (left.kind === ReflectionKind.promise && right.kind === ReflectionKind.promise) return isExtendable(left.type, right.type);
if (left.kind === ReflectionKind.promise && right.kind === ReflectionKind.promise) return _isExtendable(left.type, right.type);

if (left.kind === ReflectionKind.promise && right.kind === ReflectionKind.object) return true;

if (left.kind === ReflectionKind.promise) return isExtendable(createPromiseObjectLiteral(left.type), right);
if (right.kind === ReflectionKind.promise) return isExtendable(left, createPromiseObjectLiteral(right.type));
if (left.kind === ReflectionKind.promise) return _isExtendable(createPromiseObjectLiteral(left.type), right);
if (right.kind === ReflectionKind.promise) return _isExtendable(left, createPromiseObjectLiteral(right.type));

if (right.kind !== ReflectionKind.union) {
if (left.kind === ReflectionKind.null) {
Expand Down Expand Up @@ -157,7 +172,7 @@ export function isExtendable(leftValue: AssignableType, rightValue: AssignableTy
if ('string' === typeof left.literal && right.kind === ReflectionKind.templateLiteral) {
return extendTemplateLiteral(left, right);
}
if (right.kind === ReflectionKind.union) return right.types.some(v => isExtendable(leftValue, v, extendStack));
if (right.kind === ReflectionKind.union) return right.types.some(v => _isExtendable(left, v, extendStack));
return false;
}

Expand All @@ -183,15 +198,15 @@ export function isExtendable(leftValue: AssignableType, rightValue: AssignableTy
if (right.kind === ReflectionKind.objectLiteral) {
for (const type of resolveTypeMembers(right)) {
if (type.kind === ReflectionKind.callSignature) {
if (isExtendable(left, type, extendStack)) return true;
if (_isExtendable(left, type, extendStack)) return true;
}
}

return false;
}

if (right.kind === ReflectionKind.function || right.kind === ReflectionKind.methodSignature || right.kind === ReflectionKind.method) {
const returnValid = isExtendable(left.return, right.return, extendStack);
const returnValid = _isExtendable(left.return, right.return, extendStack);
if (!returnValid) return false;

return isFunctionParameterExtendable(left, right, extendStack);
Expand All @@ -201,12 +216,12 @@ export function isExtendable(leftValue: AssignableType, rightValue: AssignableTy
}

if ((left.kind === ReflectionKind.propertySignature || left.kind === ReflectionKind.property) && (right.kind === ReflectionKind.propertySignature || right.kind === ReflectionKind.property)) {
return isExtendable(left.type, right.type, extendStack);
return _isExtendable(left.type, right.type, extendStack);
}

if ((left.kind === ReflectionKind.class || left.kind === ReflectionKind.objectLiteral) && right.kind === ReflectionKind.function && right.name === 'new') {
const leftConstructor = (left.types as Type[]).find(v => (v.kind === ReflectionKind.method && v.name === 'constructor') || (v.kind === ReflectionKind.methodSignature && v.name === 'new'));
const valid = isExtendable(right, leftConstructor || { kind: ReflectionKind.function, parameters: [], return: { kind: ReflectionKind.any } }, extendStack);
const valid = _isExtendable(right, leftConstructor || { kind: ReflectionKind.function, parameters: [], return: { kind: ReflectionKind.any } }, extendStack);
return valid;
}

Expand Down Expand Up @@ -234,7 +249,7 @@ export function isExtendable(leftValue: AssignableType, rightValue: AssignableTy
}
}

return isExtendable(left, rightConstructor.return, extendStack);
return _isExtendable(left, rightConstructor.return, extendStack);
}

for (const member of right.types) {
Expand All @@ -245,7 +260,7 @@ export function isExtendable(leftValue: AssignableType, rightValue: AssignableTy
if (member.name === 'constructor') continue;
const leftMember = (left.types as Type[]).find(v => isMember(v) && v.name === member.name);
if (!leftMember) return false;
if (!isExtendable(leftMember, member, extendStack)) {
if (!_isExtendable(leftMember, member, extendStack)) {
return false;
}
}
Expand All @@ -263,7 +278,7 @@ export function isExtendable(leftValue: AssignableType, rightValue: AssignableTy


if (left.kind === ReflectionKind.array && right.kind === ReflectionKind.array) {
return isExtendable(left.type, right.type, extendStack);
return _isExtendable(left.type, right.type, extendStack);
}

if (left.kind === ReflectionKind.tuple && right.kind === ReflectionKind.array) {
Expand All @@ -273,7 +288,7 @@ export function isExtendable(leftValue: AssignableType, rightValue: AssignableTy
const type = member.type.kind === ReflectionKind.rest ? member.type.type : member.type;
if (isTypeIncluded(tupleUnion.types, type)) tupleUnion.types.push(type);
}
return isExtendable(tupleUnion, right, extendStack);
return _isExtendable(tupleUnion, right, extendStack);
}

if (left.kind === ReflectionKind.array && right.kind === ReflectionKind.tuple) {
Expand All @@ -282,7 +297,7 @@ export function isExtendable(leftValue: AssignableType, rightValue: AssignableTy
for (const member of right.types) {
let type = member.type.kind === ReflectionKind.rest ? member.type.type : member.type;
if (member.optional) type = flatten({ kind: ReflectionKind.union, types: [{ kind: ReflectionKind.undefined }, type] });
if (!isExtendable(left.type, type, extendStack)) return false;
if (!_isExtendable(left.type, type, extendStack)) return false;
}
return true;
}
Expand All @@ -292,17 +307,17 @@ export function isExtendable(leftValue: AssignableType, rightValue: AssignableTy
const rightType = indexAccess(right, { kind: ReflectionKind.literal, literal: i });
const leftType = indexAccess(left, { kind: ReflectionKind.literal, literal: i });
if (rightType.kind === ReflectionKind.infer || leftType.kind === ReflectionKind.infer) continue;
const valid = isExtendable(leftType, rightType, extendStack);
const valid = _isExtendable(leftType, rightType, extendStack);
if (!valid) return false;
}
inferFromTuple(left, right);

return true;
}

if (left && left.kind === ReflectionKind.union) return left.types.every(v => isExtendable(v, rightValue, extendStack));
if (left && left.kind === ReflectionKind.union) return left.types.every(v => _isExtendable(v, right, extendStack));

if (right.kind === ReflectionKind.union) return right.types.some(v => isExtendable(leftValue, v, extendStack));
if (right.kind === ReflectionKind.union) return right.types.some(v => _isExtendable(left, v, extendStack));

return false;
} finally {
Expand Down Expand Up @@ -383,7 +398,7 @@ function isFunctionParameterExtendable(left: { parameters: TypeParameter[] }, ri
//we have to change the position here since its type assignability is inversed to tuples rules
// true for tuple: [a: string] extends [a: string, b: string]
// false for function: (a: string) extends (a: string, b: string)
const valid = isExtendable(rightTuple, leftTuple, extendStack);
const valid = _isExtendable(rightTuple, leftTuple, extendStack);
if (valid) {
inferFromTuple(leftTuple, rightTuple);
}
Expand Down
13 changes: 13 additions & 0 deletions packages/type/src/reflection/processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
narrowOriginalLiteral,
ReflectionKind,
ReflectionVisibility,
stringifyType,
Type,
TypeBaseMember,
TypeCallSignature,
Expand Down Expand Up @@ -333,6 +334,17 @@ export class Processor {
};

reflect(object: ClassType | Function | Packed | any, inputs: RuntimeStackEntry[] = [], options: ReflectOptions = {}): Type {
const start = Date.now();
const result = this._reflect(object, inputs, options);

const took = Date.now() - start;
if (took > 100) {
console.warn(`Type computation took very long ${took}ms for ${stringifyType(result)}`);
}
return result;
}

_reflect(object: ClassType | Function | Packed | any, inputs: RuntimeStackEntry[] = [], options: ReflectOptions = {}): Type {
const packed: Packed | undefined = isPack(object) ? object : object.__type;
if (!packed) {
if (isFunction(object) && object.length === 0) {
Expand Down Expand Up @@ -1690,6 +1702,7 @@ export function typeInfer(value: any): Type {
if (isArray(value.__type)) {
//with emitted types: function or class
//don't use resolveRuntimeType since we don't allow cache here
// console.log('typeInfer of', value.name);
return Processor.get().reflect(value) as Type;
}

Expand Down

0 comments on commit 8c79e4b

Please sign in to comment.