0

Trying to come up with API, mixing in promise functionality like:

class Awaitable {
  constructor () {
    this.promise = Promise.resolve()
  }
  then (fn) {
    // awaited result must be _this_ instance
    return this.promise.then(() => fn(this))
  }
}

let smth = await (new Awaitable())
console.log(smth)

This code creates recursion. The main point is to have smth to be the newly created thenable instance.

Stubbing then with null makes awaited result incomplete.

I wonder if that's possible at all, seems like there's some conceptual hurdle, I can't wrap my head around.

dy_
  • 6,462
  • 5
  • 24
  • 30
  • 1
    No, you cannot resolve a promise with itself. – Bergi Sep 10 '19 at 21:30
  • It seems like not only with itself, but with any thenable in general. Is there any workaround? – dy_ Sep 10 '19 at 21:32
  • 1
    What is your actual main point? What are you trying to do here? Why do you need an `Awaitable` class? You don't *want* to create the recursion, right? – Bergi Sep 10 '19 at 21:33
  • Imagine jQuery to be awaitable and return set when all effects are done `let $target = await $(target).fadeIn(100)` – dy_ Sep 10 '19 at 21:34
  • 2
    The jQuery wrapper in fact *is* awaitable, it just takes calling an extra method: [`.promise()`](https://api.jquery.com/promise/). But the resulting promise fulfills with `undefined` afaik, not with the jQuery wrapper itself. – Bergi Sep 10 '19 at 21:37
  • That's nice workaround, but hoped to find a proof the initial or similar API is not possible. I believe that could be useful pattern for organizing APIs. – dy_ Sep 10 '19 at 21:47
  • The promise returned by `jQueryObj.promise()` resolves with `jQueryObj`. See [first example here](https://api.jquery.com/promise/). – Roamer-1888 Sep 10 '19 at 21:47
  • Yes, TY, the point is not jQuery, but the pattern for organizing such APIs - not 100% that jQuery way is the standard. – dy_ Sep 10 '19 at 21:50
  • Hard to work out what you are trying to achieve that's not already provided by promises with or without async/await. – Roamer-1888 Sep 10 '19 at 21:55

2 Answers2

0

It makes no sense for a promise (or any thenable) to fulfill with itself. If you already had the object, you wouldn't need to wait for it.

Resolving a promise does automatically unwrap thenables, which would create infinite recursion in your case where it would resolve with itself, and you can (unfortunately) not avoid that. So just don't attempt to do it. Fulfill your promise with nothing (undefined) instead.

class Awaitable {
  constructor () {
    this.promise = Promise.resolve(undefined); // simplified
  }
  then(onfulfill, onreject) {
    return this.promise.then(onfulfill, onreject);
  }
  // other methods
}

const smth = new Awaitable();
await smth; // works just fine now
console.log(smth);
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Yes, but what is achieved that's not already available from `const smth = Promise.resolve(undefined);` in the usual way? Maybe if the OP could explain "pattern for organizing such APIs" .... – Roamer-1888 Sep 10 '19 at 23:45
  • @Roamer-1888 I would expect the thing to wrap a more complicated promise, and to have more methods than just `then`. Otherwise yes, one would just use the plain promise. – Bergi Sep 10 '19 at 23:48
  • @Roamer-1888 by pattern I mean `let s = await (new Something()).asyncA().asyncB()` - that is very handy for organizing queues of tasks per instance. That can be anything - from database connectors to backend APIs. In particular, I'm implementing that in https://github.com/spectjs/spect. I think I'll try dynamic `then` method - if there's something scheduled, the instance is thenable, otherwise not - await doesn't cause recursion that way. I'll try to come up with an answer. – dy_ Sep 11 '19 at 00:21
  • 1
    @dy_, at a tangent to what you are asking here, you might be interested in the [Fantasy Land Specification](https://github.com/fantasyland/fantasy-land). – Roamer-1888 Sep 11 '19 at 00:37
0

The correct solution

Symbol.thenable proposal.

import { parse as parseStack } from 'stacktrace-parser' 

class Awaitable {
  constructor () {
    this.promise = Promise.resolve()
  },

  [Symbol.thenable]: false,

  then(fn) {
    this.promise.then(() => fn(this))
    return this
  }
}

let smth = await (new Awaitable())
console.log(smth.then) // function

Fragile non-standard solution

Detecting if thenable instance is called by await is possible by parsing callstack. Here's solution based on stacktrace-parser package:

import { parse as parseStack } from 'stacktrace-parser' 

class Awaitable {
  constructor () {
    this.promise = Promise.resolve()
  }
}

Object.defineProperty(Awaitable.prototype, 'then', {
  get() {
    let stack = parseStack((new Error).stack)
    
    // naive criteria: if stacktrace is leq 3, that's async recursion, bail out
    // works in webkit, FF/nodejs needs better heuristic
    if (stack.length <= 3) return null
    
    return (fn) => {
      this.promise.then(() => {
        fn(this)
      })
      return this
    }
  }
})

let smth = await (new Awaitable())
console.log(smth.then) // function

The heuristic must be enhanced for FF/nodejs, to solidify - that'd require sort of static analysis wizardy.

dy_
  • 6,462
  • 5
  • 24
  • 30