Search code examples
javascriptecmascript-6es6-promisees6-proxy

Promise as a class method call triggers object.then upon resolving


I have an class that contains db methods and it's wrapped in a proxy which handles access to properties. Since the issue is related to promises here is an simplified example code that is reproducing the same issue:

const handler = {
  ownKeys(target) {
    return Object.keys(target._attributes)
  },
  get(target, property) {
    console.log(`<-${property}`) // <-- this logs what properties are being accessed
    if (typeof target[property] !== 'undefined') {
        return Reflect.get(target, property)
    }
    return Reflect.get(target._attributes, property)
  },
  set(target, property, value) {
    target._attributes[property] = value
    return true
  }
}
class User {
    static table = 'users'
    static fetch(query = {}, opts = {}) {
        return new Promise((resolve, reject) => {
          setTimeout(() => {
            resolve(new this(query))
          }, 500)
        })
    }
    constructor(attributes = {}) {
        this._attributes = attributes
        return new Proxy(this, handler)
    }
}
async function trigger() {
  const user = await User.fetch({test:'test'})
  console.log(JSON.stringify(user._attributes))
}
trigger()

Everything works well, during the testing I've added a printout to the proxy to determine performance hit of using such model design, and I noticed that my model get's called from within promise chain.

Example output follows:

<-then
<-_attributes
{"test":"test"}

I guess that returning new this(query) causes the promises to think that maybe it's a promise returned and consequently .then() is executed. Only workaround that I've found is to wrap resolve response inside new array or another object like this:

static fetch(query = {}, opts = {}) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve([new this(query)])
    }, 500)
  })
}
// Output
// 2
// <-_attributes
// {"test":"test"}

What I'm wondering is, is this correct way and are there other solutions to this side effect?


Solution

  • All objects passed to as a result of a promise is always checked to see if it has a then property. If it does, that function is used to queue up entries to get a final value. This is why logic like

    Promise.resolve()
       .then(() => {
           return Promise.resolve(45);
       })
       .then(result => {
           console.log(result);
       });
    

    logs 45 instead of a promise object. Since the promise objects has a .then property, it is used to unwrap the promises value. The same behavior happens in your resolve(new this(query)) case, because it needs to know if there is a value to unwrap.

    As you've said, commented in your post, you could certainly wrap the instance with a non-proxy, e.g.

    resolve({ value: new this(query) })
    

    which would check for .then on that object instead of on your proxy, but then you have to do .value to actually get the proxy, which can be a pain.

    At the end of the day, that is a choice you'll have to make.