Skip to content

Commit

Permalink
feature(type): make all types optional nominal.
Browse files Browse the repository at this point in the history
This allows to differentiate between

    type SomeType = any;
    type SomeOtherType = any;

in the DI container.

This introduces a new bytecode OP "nominal" which gives the last expression on the stack a unique nominal ID.
  • Loading branch information
marcj committed Nov 21, 2023
1 parent 2b277b1 commit 7b0c262
Show file tree
Hide file tree
Showing 18 changed files with 674 additions and 208 deletions.
33 changes: 33 additions & 0 deletions packages/app/tests/service-container.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { expect, test } from '@jest/globals';
import { AppModule, createModule } from '../src/module.js';
import { ServiceContainer } from '../src/service-container.js';
import { InjectorContext } from '@deepkit/injector';

test('simple setup with import and overwrite', () => {
class Connection {
Expand Down Expand Up @@ -272,3 +273,35 @@ test('exported module', () => {
expect(databaseModuleInjector.get(DatabaseConnection)).toBe(rootInjector.get(DatabaseConnection));
}
});

test('scoped InjectorContext access', () => {
class RpcInjectorContext extends InjectorContext {
}

class MyService {
constructor(public injectorContext: InjectorContext) {
}
}

const myModule = new AppModule({
providers: [
{ provide: MyService },
{ provide: RpcInjectorContext, scope: 'rpc', useValue: undefined },
]
});

const serviceContainer = new ServiceContainer(myModule);
{
const injector = serviceContainer.getInjectorContext();
const myService = injector.get(MyService);
expect(myService.injectorContext).toBeInstanceOf(InjectorContext);
}

{
const injector = serviceContainer.getInjectorContext().createChildScope('rpc');
injector.set(RpcInjectorContext, injector);

const myService = injector.get(MyService);
expect(myService.injectorContext).toBeInstanceOf(InjectorContext);
}
});
8 changes: 3 additions & 5 deletions packages/framework/src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,7 @@ export class FrameworkModule extends createModule({
}
},

{
provide: BrokerDeepkitAdapter, useFactory: (config: BrokerConfig) => {
new BrokerDeepkitAdapter({ servers: [{ url: config.host }] });
}
},
{ provide: BrokerDeepkitAdapter, useFactory: (config: BrokerConfig) => new BrokerDeepkitAdapter({ servers: [{ url: config.host }] }) },
{ provide: BrokerCache, useFactory: (adapter: BrokerDeepkitAdapter) => new BrokerCache(adapter) },
{ provide: BrokerLock, useFactory: (adapter: BrokerDeepkitAdapter) => new BrokerLock(adapter) },
{ provide: BrokerQueue, useFactory: (adapter: BrokerDeepkitAdapter) => new BrokerQueue(adapter) },
Expand Down Expand Up @@ -146,6 +142,8 @@ export class FrameworkModule extends createModule({
BrokerBus,
BrokerServer,

FilesystemRegistry,

HttpModule,
]
}, 'framework') {
Expand Down
117 changes: 61 additions & 56 deletions packages/injector/src/injector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,8 @@ function createTransientInjectionTarget(destination: Destination | undefined) {
return new TransientInjectionTarget(destination.token);
}

let CircularDetector: any[] = [];
let CircularDetectorResets: (() => void)[] = [];
const CircularDetector: any[] = [];
const CircularDetectorResets: (() => void)[] = [];

function throwCircularDependency() {
const path = CircularDetector.map(tokenLabel).join(' -> ');
Expand Down Expand Up @@ -184,6 +184,20 @@ export function resolveToken(provider: ProviderWithScope): Token {
return provider.provide;
}

export function getContainerToken(type: Token): string | number | symbol | bigint | boolean | RegExp | ClassType | Function {
if (isClass(type)) return type;
if (type instanceof TagProvider) return getContainerToken(type.provider);

if (isType(type)) {
if (type.id) return type.id;
if (type.kind === ReflectionKind.literal) return type.literal;
if (type.kind === ReflectionKind.class) return type.classType;
if (type.kind === ReflectionKind.function && type.function) return type.function;
}

return type;
}

export interface InjectorInterface {
get<T>(token: T, scope?: Scope): ResolveToken<T>;
}
Expand Down Expand Up @@ -268,7 +282,7 @@ export class Injector implements InjectorInterface {
if (!this.resolver) throw new Error('Injector was not built');
if ('string' === typeof token || 'number' === typeof token || 'bigint' === typeof token ||
'boolean' === typeof token || 'symbol' === typeof token || isFunction(token) || isClass(token) || token instanceof RegExp) {
return this.resolver(token, scope) as ResolveToken<T>;
return this.resolver(getContainerToken(token), scope) as ResolveToken<T>;
} else if (isType(token)) {
return this.createResolver(isType(token) ? token as Type : resolveReceiveType(token), scope)(scope);
} else if (isArray(token)) {
Expand Down Expand Up @@ -333,33 +347,36 @@ export class Injector implements InjectorInterface {
const scopeObjectCheck = scope ? ` && scope && scope.name === ${JSON.stringify(scope)}` : '';
const scopeCheck = scope ? ` && scope === ${JSON.stringify(scope)}` : '';

setterLines.push(`case token === ${setterCompiler.reserveVariable('token', prepared.token)}${scopeObjectCheck}: {
const diToken = getContainerToken(prepared.token);

setterLines.push(`case token === ${setterCompiler.reserveVariable('token', diToken)}${scopeObjectCheck}: {
if (${accessor} === undefined) {
injector.instantiated.${name} = injector.instantiated.${name} ? injector.instantiated.${name} + 1 : 1;
}
${accessor} = value;
break;
}`);


if (prepared.resolveFrom) {
//its a redirect
//it's a redirect
lines.push(`
case token === ${resolverCompiler.reserveConst(prepared.token, 'token')}${scopeObjectCheck}: {
return ${resolverCompiler.reserveConst(prepared.resolveFrom, 'resolveFrom')}.injector.resolver(${resolverCompiler.reserveConst(prepared.token, 'token')}, scope, destination);
case token === ${resolverCompiler.reserveConst(diToken, 'token')}${scopeObjectCheck}: {
return ${resolverCompiler.reserveConst(prepared.resolveFrom, 'resolveFrom')}.injector.resolver(${resolverCompiler.reserveConst(diToken, 'token')}, scope, destination);
}
`);

instantiationLines.push(`
case token === ${instantiationCompiler.reserveConst(prepared.token, 'token')}${scopeCheck}: {
return ${instantiationCompiler.reserveConst(prepared.resolveFrom, 'resolveFrom')}.injector.instantiations(${instantiationCompiler.reserveConst(prepared.token, 'token')}, scope);
case token === ${instantiationCompiler.reserveConst(diToken, 'token')}${scopeCheck}: {
return ${instantiationCompiler.reserveConst(prepared.resolveFrom, 'resolveFrom')}.injector.instantiations(${instantiationCompiler.reserveConst(diToken, 'token')}, scope);
}
`);
} else {
//we own and instantiate the service
lines.push(this.buildProvider(buildContext, resolverCompiler, name, accessor, scope, prepared.token, provider, prepared.modules));

instantiationLines.push(`
case token === ${instantiationCompiler.reserveConst(prepared.token, 'token')}${scopeCheck}: {
case token === ${instantiationCompiler.reserveConst(diToken, 'token')}${scopeCheck}: {
return injector.instantiated.${name} || 0;
}
`);
Expand Down Expand Up @@ -412,7 +429,7 @@ export class Injector implements InjectorInterface {
) {
let transient = false;
let factory: { code: string, dependencies: number } = { code: '', dependencies: 0 };
const tokenVar = compiler.reserveConst(token);
const tokenVar = compiler.reserveConst(getContainerToken(token));

if (isValueProvider(provider)) {
transient = provider.transient === true;
Expand All @@ -431,7 +448,7 @@ export class Injector implements InjectorInterface {
factory = this.createFactory(provider, accessor, compiler, useClass, resolveDependenciesFrom);
} else if (isExistingProvider(provider)) {
transient = provider.transient === true;
factory.code = `${accessor} = injector.resolver(${compiler.reserveConst(provider.useExisting)}, scope, destination)`;
factory.code = `${accessor} = injector.resolver(${compiler.reserveConst(getContainerToken(provider.useExisting))}, scope, destination)`;
} else if (isFactoryProvider(provider)) {
transient = provider.transient === true;
const args: string[] = [];
Expand Down Expand Up @@ -471,7 +488,7 @@ export class Injector implements InjectorInterface {
for (const arg of call.args) {
if (arg instanceof InjectorReference) {
const injector = arg.module ? compiler.reserveConst(arg.module) + '.injector' : 'injector';
args.push(`${injector}.resolver(${compiler.reserveConst(arg.to)}, scope, destination)`);
args.push(`${injector}.resolver(${compiler.reserveConst(getContainerToken(arg.to))}, scope, destination)`);
} else {
args.push(`${compiler.reserveVariable('arg', arg)}`);
}
Expand All @@ -484,7 +501,7 @@ export class Injector implements InjectorInterface {
let value: string = '';
if (call.value instanceof InjectorReference) {
const injector = call.value.module ? compiler.reserveConst(call.value.module) + '.injector' : 'injector';
value = `${injector}.resolver(${compiler.reserveConst(call.value.to)}, scope, destination)`;
value = `${injector}.resolver(${compiler.reserveConst(getContainerToken(call.value.to))}, scope, destination)`;
} else {
value = compiler.reserveVariable('value', call.value);
}
Expand Down Expand Up @@ -613,7 +630,7 @@ export class Injector implements InjectorInterface {
const entries = this.buildContext.tagRegistry.resolve(options.type.classType);
const args: string[] = [];
for (const entry of entries) {
args.push(`${compiler.reserveConst(entry.module)}.injector.resolver(${compiler.reserveConst(entry.tagProvider.provider.provide)}, scope, ${destinationVar})`);
args.push(`${compiler.reserveConst(entry.module)}.injector.resolver(${compiler.reserveConst(getContainerToken(entry.tagProvider.provider.provide))}, scope, ${destinationVar})`);
}
return `new ${tokenVar}(${resolvedVar} || (${resolvedVar} = [${args.join(', ')}]))`;
}
Expand Down Expand Up @@ -678,25 +695,22 @@ export class Injector implements InjectorInterface {
}
}

let findToken: Token = options.type;
if (isType(findToken)) {
if (findToken.kind === ReflectionKind.class) {
findToken = findToken.classType;
} else if (findToken.kind === ReflectionKind.literal) {
findToken = findToken.literal;
}
}

let foundPreparedProvider: PreparedProvider | undefined = undefined;
for (const module of resolveDependenciesFrom) {
foundPreparedProvider = module.getPreparedProvider(findToken);
foundPreparedProvider = module.getPreparedProvider(options.type, foundPreparedProvider);
}

if (resolveDependenciesFrom[0] !== this.module) {
//the provider was exported from another module, so we need to check if there is a more specific candidate
foundPreparedProvider = this.module.getPreparedProvider(options.type, foundPreparedProvider);
}

if (!foundPreparedProvider) {
//try if parents have anything
const foundInModule = this.module.resolveToken(findToken);
if (foundInModule) {
foundPreparedProvider = foundInModule.getPreparedProvider(findToken);
//go up parent hierarchy
let current: InjectorModule | undefined = this.module;
while (current && !foundPreparedProvider) {
foundPreparedProvider = current.getPreparedProvider(options.type, foundPreparedProvider);
current = current.parent;
}
}

Expand All @@ -714,23 +728,20 @@ export class Injector implements InjectorInterface {
const type = stringifyType(options.type, { showFullDefinition: false }).replace(/\n/g, '').replace(/\s\s+/g, ' ').replace(' & InjectMeta', '');
if (options.optional) return 'undefined';
throw new DependenciesUnmetError(
`Undefined dependency "${options.name}: ${type}" of ${of}. Type has no provider in ${fromScope ? 'scope ' + fromScope : 'no scope'}.`
`Undefined dependency "${options.name}: ${type}" of ${of}. Type has no provider${fromScope ? ' in scope ' + fromScope : ''}.`
);

// throw new DependenciesUnmetError(
// `Unknown dependency '${options.name}: ${stringifyType(options.type, { showFullDefinition: false })}' of ${of}.`
// );
}

const tokenVar = compiler.reserveVariable('token', foundPreparedProvider.token);
const tokenVar = compiler.reserveVariable('token', getContainerToken(foundPreparedProvider.token));
const allPossibleScopes = foundPreparedProvider.providers.map(getScope);
const unscoped = allPossibleScopes.includes('') && allPossibleScopes.length === 1;
const foundProviderLabel = foundPreparedProvider.providers.map(v => v.provide).map(tokenLabel).join(', ');

if (!unscoped && !allPossibleScopes.includes(fromScope)) {
const t = stringifyType(options.type, { showFullDefinition: false });
throw new DependenciesUnmetError(
`Dependency '${options.name}: ${t}' of ${of} can not be injected into ${fromScope ? 'scope ' + fromScope : 'no scope'}, ` +
`since ${t} only exists in scope${allPossibleScopes.length === 1 ? '' : 's'} ${allPossibleScopes.join(', ')}.`
`since ${foundProviderLabel} only exists in scope${allPossibleScopes.length === 1 ? '' : 's'} ${allPossibleScopes.join(', ')}.`
);
}

Expand All @@ -740,7 +751,7 @@ export class Injector implements InjectorInterface {

const resolveFromModule = foundPreparedProvider.resolveFrom || foundPreparedProvider.modules[0];
if (resolveFromModule === this.module) {
return `injector.resolver(${tokenVar}, scope, ${destinationVar})`;
return `injector.resolver(${tokenVar}, scope, ${destinationVar}) ${orThrow}`;
}
return `${compiler.reserveConst(resolveFromModule)}.injector.resolver(${tokenVar}, scope, ${destinationVar}) ${orThrow}`;
}
Expand Down Expand Up @@ -833,26 +844,22 @@ export class Injector implements InjectorInterface {
}
}

let findToken: Token = type;

if (isType(findToken)) {
if (findToken.kind === ReflectionKind.class) {
findToken = findToken.classType;
} else if (findToken.kind === ReflectionKind.literal) {
findToken = findToken.literal;
}
}

let foundPreparedProvider: PreparedProvider | undefined = undefined;
for (const module of resolveDependenciesFrom) {
foundPreparedProvider = module.getPreparedProvider(findToken);
foundPreparedProvider = module.getPreparedProvider(type, foundPreparedProvider);
}

if (resolveDependenciesFrom[0] !== this.module) {
//the provider was exported from another module, so we need to check if there is a more specific candidate
foundPreparedProvider = this.module.getPreparedProvider(type, foundPreparedProvider);
}

if (!foundPreparedProvider) {
//try if parents have anything
const foundInModule = this.module.resolveToken(findToken);
if (foundInModule) {
foundPreparedProvider = foundInModule.getPreparedProvider(findToken);
//go up parent hierarchy
let current: InjectorModule | undefined = this.module;
while (current && !foundPreparedProvider) {
foundPreparedProvider = current.getPreparedProvider(type, foundPreparedProvider);
current = current.parent;
}
}

Expand All @@ -879,7 +886,7 @@ export class Injector implements InjectorInterface {

const resolveFromModule = foundPreparedProvider.resolveFrom || foundPreparedProvider.modules[0];

return (scopeIn?: Scope) => resolveFromModule.injector!.resolver!(foundPreparedProvider!.token, scopeIn || scope);
return (scopeIn?: Scope) => resolveFromModule.injector!.resolver!(getContainerToken(foundPreparedProvider!.token), scopeIn || scope);
}
}

Expand All @@ -904,9 +911,7 @@ export class BuildContext {
globalSetupProviderRegistry: SetupProviderRegistry = new SetupProviderRegistry;
}

export interface Resolver<T> {
(scope?: Scope): T;
}
export type Resolver<T> = (scope?: Scope) => T;

/**
* A InjectorContext is responsible for taking a root InjectorModule and build all Injectors.
Expand Down
Loading

0 comments on commit 7b0c262

Please sign in to comment.