Joshua de Leon
Joshua de Leon

Reputation: 59

Async/await confusion using singeltons

So no matter what I've read, even once I do it right, I can't seem to get the the hang of async and await. For example I have this in my startup.

startup.js

  await CommandBus.GetInstance();
  await Consumers.GetInstance();

Debugging jumps to the end of the get instance for CommandBus (starting up a channel for rabbitmq) and start Consumers.GetInstance() which fails since channel is null. CommandBus.js

export default class CommandBus {
  private static instance: CommandBus;
  private channel: any;
  private conn: Connection;

  private constructor() {
    this.init();
  }

  private async init() {
    //Create connection to rabbitmq
    console.log("Starting connection to rabbit.");
    this.conn = await connect({
      protocol: "amqp",
      hostname: settings.RabbitIP,
      port: settings.RabbitPort,
      username: settings.RabbitUser,
      password: settings.RabbitPwd,
      vhost: "/"
    });

    console.log("connecting channel.");
    this.channel = await this.conn.createChannel();
  }

  static async GetInstance(): Promise<CommandBus> {
    if (!CommandBus.instance) {
      CommandBus.instance = new CommandBus();
    }

    return CommandBus.instance;
  }
  public async AddConsumer(queue: Queues) {
    await this.channel.assertQueue(queue);
    this.channel.consume(queue, msg => {
      this.Handle(msg, queue);
    });
  }
}

Consumers.js

export default class Consumers {
  private cb: CommandBus;
  private static instance: Consumers;

  private constructor() {
    this.init();
  }

  private async init() {
    this.cb = await CommandBus.GetInstance();
    await cb.AddConsumer(Queues.AuthResponseLogin);
  }

  static async GetInstance(): Promise<Consumers> {
    if (!Consumers.instance) {
      Consumers.instance = new Consumers();
    }

    return Consumers.instance;
  }
}

Sorry I realize this is in Typescript, but I imagine that doesn't matter. The issue occurs specifically when calling cb.AddConsumer which can be found CommandBus.js. It tries to assert a queue against a channel that doesn't exist yet. What I don't understand, is looking at it. I feel like I've covered all the await areas, so that it should wait on channel creation. The CommandBus is always fetched as a singleton. I don't if this poses issues, but again it is one of those areas that I cover with awaits as well. Any help is great thanks everyone.

Upvotes: 0

Views: 458

Answers (1)

jfriend00
jfriend00

Reputation: 707876

You can't really use asynchronous operations in a constructor. The problem is that the constructor needs to return your instance so it can't also return a promise that will tell the caller when it's done.

So, in your Consumers class, await new Consumers(); is not doing anything useful. new Consumers() returns a new instance of a Consumers object so when you await that it doesn't actually wait for anything. Remember that await does something useful with you await a promise. It doesn't have any special powers to await your constructor being done.

The usual way around this is to create a factory function (which can be a static in your design) that returns a promise that resolves to the new object.

Since you're also trying to make a singleton, you would cache the promise the first time you create it and always return the promise to the caller so the caller would always use .then() to get the finished instance. The first time they call it, they'd get a promise that was still pending, but later they'd get a promise that was already fulfilled. In either case, they just use .then() to get the instance.

I don't know TypeScript well enough to suggest to you the actual code for doing this, but hopefully you get the idea from the description. Turn GetInstance() into a factory function that returns a promise (that you cache) and have that promise resolve to your instance.

Something like this:

static async GetInstance(): Promise<Consumers> {
  if (!Consumers.promise) {
      let obj = new Consumers();
      Consumers.promise = obj.init().then(() => obj);
  }
  return Consumers.promise;
}

Then, the caller would do:

Consumers.getInstance().then(consumer => {
    // code here to use the singleton consumer object
}).catch(err => {
    console.log("failed to get consumer object");
});

You will have to do the same thing in any class that has async operations involved in initializing the object (like CommandBus) too and each .init() call needs to call the base class super.init().then(...) so base class can do its thing to get properly initialized too and the promise your .init() is linked to the base class too. Or, if you're creating other objects that themselves have factory functions, then your .init() needs to call those factory functions and link their promises together so the .init() promise that is returned is linked to the other factory function promises too (so the promise your .init() returns will not resolve until all dependent objects are all done).

Upvotes: 1

Related Questions