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?
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.