Aurelia 2, DI what are the different registrations for?

  • Registration.singleton() - ok this one is obvious, it creates a singleton
  • Registration.instance() - I guess my question about this is, if I associate a value with it, is a new copy returned each time? how is it not a singleton?
  • Registration.callback() - weird name, this obviously takes a factory method, is it called each time the object is requested? is their a way to cache it so it’s only called once?
  • Registraton.alias() - simple it maps 2 keys together so you can get the same object with either.
  • Registration.defer() - I have no idea
  • Registration.transient() - creates a new instance each time using the constructor

can anyone fill me in on the questions I have?

2 Likes

@xenoterracide This is a nice question.

how is it not a singleton?

You are right that singleton and instance is similar. In fact under the hood, the singleton strategy is lazily converted to instance strategy. Having said that, instance is super useful when you have a class that you want to instantiate in a particular way. For example, if you have non-injectable constructor arguments, the singleton strategy may end up injecting undefined for those args, which is certainly not useful.

The instance strategy is very useful in this case. You create an instance and register that with the key. And DI returns the same instance every time.

called each time the object is requested?

AFAIK yes

is their a way to cache it so it’s only called once?

AFAIK no. But then it is not much of a callback, is it? If you want to do it once, better use the instance strategy :slight_smile:

For the rest pinging @fkleuver and @EisenbergEffect

1 Like

callback seems useful as a factory, when I need to programmatically instantiate an object, say from a 3rd party library. That doesn’t mean I don’t want it to end up as a singleton… though maybe there’s a better way to do this.

Why not write you callback in such a way that it caches the instance by itself?

I am just trying to understand your use-case. Can you please give a concrete example?

sure, I’m seeing how migrating from tsyringe goes, in tsyringe there’s a lot of this, essentially they made a helper method around caching your factories if you want it (note: in this case I’ve already written an API compatibility layer for Aurelia, that’s why those imports aren’t coming from tsyringe.) We have code all over the place that uses factories mostly because of 3rd party code like typeorm, winston, etc, and most of those objects we ultimately don’t need/want more than one instance.

import { LoggingWinston } from '@google-cloud/logging-winston';
import winston, { format, Logger, LoggerOptions, transports } from 'winston';
import { instanceCachingFactory, registry } from '../injection/injectionutils';

export const LoggerDefault = 'defaultLogger';
export const LoggerTypeOrm = 'typeormLogger';
export const LoggerSecurity = 'securityLogger';
export const LoggerSecurityChild = 'childSecurityLogger';

@registry([
  { token: LoggerDefault, useFactory: instanceCachingFactory(createDefaultLogger) },
  { token: LoggerTypeOrm, useFactory: instanceCachingFactory(createTypeOrmLogger) },
  { token: LoggerSecurity, useFactory: instanceCachingFactory(createSecurityLogger) },
])
export class LoggerProvider {}

export function createDefaultLogger(): Logger {
  const config: LoggerOptions =
    process.env['NODE_ENV'] === 'development' ? devConfig() : serverConfig();

  return winston.loggers.add(LoggerDefault, config);
}

function createTypeOrmLogger(): Logger {
  const config: LoggerOptions =
    process.env['NODE_ENV'] === 'development' ? devConfig() : serverConfig();

  return winston.loggers.add(LoggerTypeOrm, config);
}

function createSecurityLogger(): Logger {
  const config: LoggerOptions =
    process.env['NODE_ENV'] === 'development' ? devConfig() : googleConfig();
  return winston.loggers.add(LoggerSecurity, config);
}

function devConfig(): LoggerOptions {
  const console = new transports.Console({
    level: process.env.LOG_LEVEL || 'info',
    debugStdout: true,
    format: format.combine(format.cli(), format.splat(), format.simple()),
  });
  return {
    transports: [console],
    exceptionHandlers: [console],
  };
}

function googleConfig(): LoggerOptions {
  return {
    transports: [
      new LoggingWinston({
        projectId: process.env.GOOGLE_SERVICE_ACCOUNT_PROJECT_ID,
        credentials: {
          client_email: process.env.GOOGLE_SERVICE_ACCOUNT_CLIENT_EMAIL!,
          private_key: process.env.GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY!,
        },
      }),
    ],
  };
}

function serverConfig(): LoggerOptions {
  return {
    transports: [
      new transports.Console({
        level: process.env.LOG_LEVEL || 'info',
        debugStdout: true,
        format: format.combine(format.splat(), format.simple()),
      }),
    ],
  };
}

A naive way would be to do something like this.

// create the caching facility
class InstanceProviderFactory {
  private instance?: any;
  public constructor(
    private readonly instanceCreator: () => any,
  ) { }
  public resolve(handler: IContainer, requestor: IContainer) { 
    return this.instance ?? (this.instance = this.instanceCreator());
  }
}

// create the factory instances
const defaultLoggerFactory = new InstanceProviderFactory(createDefaultLogger);
const typeOrmLoggerFactory = new InstanceProviderFactory(createTypeOrmLogger);

// create injection keys/tokens/symbols/decorators
export const LoggerDefault = DI.createInterface<Logger>("LoggerDefault")
  .withDefault((x) => x.callback(defaultLoggerFactory.resolve.bind(defaultLoggerFactory)));
export const LoggerTypeOrm = DI.createInterface<Logger>("LoggerTypeOrm")
  .withDefault((x) => x.callback(typeOrmLoggerFactory.resolve.bind(typeOrmLoggerFactory)));

// inject
public constructor (
  @LoggerDefault private defaultLogger: Logger,
  @LoggerTypeOrm private typeOrmLogger: Logger,
) { }

Note

  • All these are untested :slight_smile:
  • There are better ways to handle instances, using InstanceProvider but that is more involved, and it is difficult for me to suggest something on that line without testing it first.
  • There is also a concept of proper factories in AU2. I have not suggested that because the API is not yet that friendly.
  • Registration.singleton() - For newable classes or functions. Will be created once and then stored. If the value cannot be newed, this will result in an error.
  • Registration.instance() - For any value to be stored directly as-is. Will not be newed. Can be any value, DI will not touch it or validate it, so you can even associate undefined with a parameter decorator if you want.
  • Registration.callback() - A callback that is called each time the dependency is requested. You get 3 arguments:
    • handler: The container containing the registration. Use this to retrieve dependencies scoped to the original registration.
    • requestor: The container that the dependency was requested from (either the same as handler or a child container). Use this to retrieve dependencies scoped to the requesting component.
    • resolver: The (singleton) resolver instance associated with this registration. This is the thing that invokes the callback (and in the case of the other registration types, does the appropriate action for them). You could use it to store an instance on. I personally prefer to just use a WeakMap for that sort of thing though. Example of some framework code:
const factoryCache = new WeakMap<IResolver, IShadowDOMStyleFactory>();
export const IShadowDOMStyleFactory
  = DI.createInterface<IShadowDOMStyleFactory>('IShadowDOMStyleFactory')
    .withDefault(x => x.callback((handler, requestor, resolver) => {
      let factory = factoryCache.get(resolver);

      if (factory === void 0) {
        factoryCache.set(
          resolver,
          factory = ShadowDOMRegistry.createStyleFactory(handler)
        );
      }

      return factory;
    }));
  • Registraton.alias() - Map one key to another.
  • Registration.defer() - This is somewhat of an internal api, intended to associate file extensions (which are not supported by native import (yet), e.g. .css) with mechanisms to load them at runtime. You could say it’s the lightweight v2 version of v1’s loaders.
  • Registration.transient() - For newable classes or functions. Will be created on each invocation and not stored. If the value cannot be newed, this will result in an error.
1 Like

thanks, that’s very useful information.

2 more questions…

So I was doing the DI.createInterface but then encountered the problem that the string seemed to be no longer stored in the container e.g. I couldn’t do container.get<Logger>(LoggerDefault) anymore, what more code do I need to do to also have it added to the container?

on a mostly unrelated note… injection by metadadata only doesn’t seem to be working for me.

this works

@singleton
export class AmqpSender implements Sender<any, void> {
  constructor(
    @inject(LoggerDefault) private readonly log: Logger,
    @inject(AmqpChannel) private readonly channel: Channel,
  ) {}

this will fail to inject AmqpSender

@singleton()
export class OrderService {
  constructor(
    private readonly sender: AmqpSender,
    @inject(LoggerDefault) private readonly log: Logger,
  ) {}

now, reflect-metadata works and the experimental decorators, blah blah are all their, because typeorm, tsyringe, and probably something else uses that. I’m betting there is something more I have to do in setup to make this work, since I think DI has its own metadata implementation (to be split out in 0.7.0 from the looks for the source.

update: moving the reflect conversation Aurelia 2, Kernel/Metadata not detecting metadata properly, bug?