1

I am working on a project in Node - a language with which I have little familiarity.

In the project, I have a class that will be responsible for reading and writing data to a database - in this case, LevelDB. Ideally, I'd like to set-up the database connection in the constructor synchronously so that methods (writeItem, readItem, etc.) don't fail if they're called too fast. Or in other words, I don't want the constructor to return and allow the next line of code to run until all the promises are fulfilled.

I think I am either missing something fundamental to the language or there is some design pattern in node that I'm not aware of. A toy example that fails in the same way is here:

class AClass {
  constructor(n) {
    this.name = n;
    console.log('calling init.');
    this.init();
    console.log('init returned.');
  }

  func() {
    return new Promise(resolve =>  {
      setTimeout(() => {
        resolve(true);
      }, 2000);
    }); 
  }

  async init() {
    console.log('calling func()');
    let x = await this.func();
    console.log('after func(): ');
  }

}


let x = new AClass('test');
console.log(JSON.stringify(x));

This produces the output:

calling init.
calling func()
init returned.
{"name":"test"}
after func():

This is surprising to me. I would have expected:

calling init.
calling func()
after func():
init returned.
{"name":"test"}

The final objective is to instantiate a class that connects to a levelDB instance and does not return the object until that connection is made. So the code might look something like this:

let storage = new StorageObject();
storage.addItem(key, value);   // <-- this fails unless StorageObject
                               //     finishes the db setup.

Thanks! Sam

Sam
  • 507
  • 2
  • 11
  • https://stackoverflow.com/questions/46515764/how-can-i-use-async-await-at-the-top-level - In this question, OP asks *"Why does the log message inside the function execute afterwards?"*. The top answer should be helpful. – Tyler Roper Aug 19 '18 at 18:41
  • @Sam this is expected, init can be called as sync or async in same time, depends on your needs – hawk Aug 19 '18 at 18:42
  • You still can call it as async with Promise inside constructor `this.init.then()` – hawk Aug 19 '18 at 18:45
  • "*I'd like to set-up the database connection in the constructor*" - just [don't do that](https://stackoverflow.com/q/24398699/1048572) – Bergi Aug 19 '18 at 19:12
  • See [Asynchronous operations in constructor](https://stackoverflow.com/questions/49905178/asynchronous-operations-in-constructor/49906064#49906064). That answer shows a couple design patterns, including a factory function which is my recommendation because then the object is not available to the consumer at all until it is fully initialized. – jfriend00 Aug 21 '18 at 04:27

3 Answers3

3

Your objective of not returning the instance until after the connection won't work (at least not like this). The constructor's job is to create an instance and return that. Functions need to return something synchronously. If it's performing some async operation then a function can return a promise instead, but you don't want a promise from the constructor — you need the instance.

The easy way to do this is to require your object to be initialized after it's created, then the constructor can construct and return the instance and the init function is free to return a promise:

class AClass {
  constructor(n) {/* some setup */}

  func() {
    return new Promise(resolve =>  {
      setTimeout(() => {
        resolve("some name");
      }, 1000);
    }); 
  }

  async init() {
    this.name = await this.func();
    return this
  }

}

new AClass('test').init()
.then((initialized_obj) => console.log(initialized_obj))

If you're doing this in node, you could also use eventEmitters to emit an event when the instance has been initialized. Something like:

const EventEmitter = require('events');

class AClass extends EventEmitter{
  constructor(n) {
    super()
    this.init()
    .then(() => this.emit("initialized"))
  }

  func() {
    return new Promise(resolve =>  {
      setTimeout(() => {
        resolve("some name");
      }, 1000);
    }); 
  }

  async init() {
    this.name = await this.func();
    return this
  }

}

let x = new AClass('test')
x.on('initialized', () => console.log(x.name))
Mark
  • 90,562
  • 7
  • 108
  • 148
  • "*require your object to be initialized after it's created*" - why not just create the object after doing the asynchronous initialisation? Nothing forces me to call `init` on an instance. – Bergi Aug 19 '18 at 19:24
  • @bergi are you suggesting wrapping the whole thing in a larger initialization function so the initializer creates the instance and returns a promise resolving to that instance? – Mark Aug 19 '18 at 19:26
  • Correct. That can even be a static method of the class, which does the initialisation and then passes its results to the constructor. – Bergi Aug 19 '18 at 19:32
  • @MarkMeyer - Node's asynchronous design patterns are still pretty foreign feeling to me, but this works beautifully. Thanks for the quick and very helpful reply! – Sam Aug 19 '18 at 19:32
0

Two things about your code:

  1. When you call an async function (this.init()), it will be executed uptill its returned await statement and will return a promise and the control will go to next line (console.log('init returned.')). Understanding this will resolve your confusion.
  2. Code after await statement (console.log('after func(): ');) will execute only after awaited promise has been resolved.

I have repurposed your code to do what you want.

    async function AClass(n) {
        let obj = {}
        obj.func = () => {
            return new Promise(resolve => {
                setTimeout(() => {
                    resolve(true);
                }, 2000);
            });
        };
    
        obj.init = async function () {
            console.log('calling func()');
            let x = await obj.func();
            console.log('after func(): ');
        };
    
        obj.name = n;
        console.log('calling init.');
        await obj.init();
        console.log('init returned.');
        return obj;
    
    }
    
    
    let x = AClass('test');
    x.then((resolveValue) => {
        /*
        *Now the "class" has been instantiated, 
        * code to use the object of the "class" goes here
        * */
        console.log(JSON.stringify(resolveValue));
    });
dasfdsa
  • 7,102
  • 8
  • 51
  • 93
  • Where do you get the actual instance in this example? – Mark Aug 19 '18 at 19:31
  • `resolveValue` is the instance. – dasfdsa Aug 19 '18 at 19:34
  • But it's not an instance of `AClass` it's just an object. – Mark Aug 19 '18 at 19:39
  • Yeah right, but It serves the same purpose. This is the solution that came to my mind since async on constructor is not allowed. However, your solution is far more elegant. – dasfdsa Aug 19 '18 at 19:41
  • One thing you could do is make a real class `AClass` and call the function you have now`AClassFactory`. Then inside `AClassFactory` instead of `obj = {}` you would have something like `obj = new AClass(n)`. Then this would be pretty close to what @bergi is suggesting in the comments above. – Mark Aug 19 '18 at 19:56
0

If you really want the constructor to return a working AClass synchronously, you can do it by rewriting other parts of code a little. I'm assuming that the methods writeItem, readItem, etc are asynchronous. All you have to do is rewrite these methods so that they wait for the object to be initialised (if necessary) before proceeding.

For example, suppose your class looks like this:

class AClass {
  constructor(n) {
    this.name = n;
    this.init();
  }

  async init() {
    ...
  }

  async writeItem(item) {
    return await db.writeItem(item);
  }

  async readItem(itemId) {
    return await db.readItem(itemId);
  }

}

You should then be able to rewrite it as follows:

class AClass {
  constructor(n) {
    this.name = n;
    this.awaitInit = this.init();
  }

  async init() {
    ...
  }

  async writeItem(item) {
    await this.awaitInit;
    return await db.writeItem(item);
  }

  async readItem(itemId) {
    await this.awaitInit;
    return await db.readItem(itemId);
  }

}
David Knipe
  • 3,417
  • 1
  • 19
  • 19