In modern ES (and without the size constraints), it could've been written as this:
class State {
value = 0
#oncompletion
complete = () => {
if (--this.value === 0 && typeof this.#oncompletion === "function") {
this.#oncompletion()
}
}
reject = (e) => {
this.complete()
if (this.value === 0) throw e
}
constructor(oncompletion) {
this.#oncompletion = oncompletion
}
}
class PromiseProxy extends Promise {
#counts
constructor(executor, initState = false) {
super(executor)
if (initState) this.#counts = State(oncompletion)
}
then(...args) {
return super.then(...args).#adopt(this)
}
#adopt(source) {
let c = source.#counts
this.counts = c
c.value++
super.then(c.complete, c.reject)
return this
}
}
function makeRequest(args) {
function executor(resolve, reject) {
// ...
}
return args.background
? new Promise(executor)
: new PromiseProxy(executor, true)
}
Your code might not have all of the same concerns, but you see why I warned about that. (And FWIW, I didn't even know the await optimization would break us, or I would've updated much earlier to compensate.)
The simplified code you share isn't doing the really weird stuff that mithril/request is, like setting __proto__ and putting constructor on the instance to cover up a bad value on the prototype chain.
My code avoids that messiness by not needing to change the identity or prototype of objects it tracks... (otherwise it seems the same)
The key isn't prototypes (which don't actually matter if you read the spec 123456), but having specific properties set.
Class functions implicitly have their own prototype set to their superclass function. This is no different from setting Class.__proto__ = Super, just done implicitly.
Promise.prototype.then observably does this.constructor?.[Symbol.species] to get the result class to construct. It doesn't matter where those properties are, all I need is it to return a promise. (This is why it delegates.)
For the implicit Promise.resolve(p) inherent to both .then(v => m.request(...)) and await m.request(...), I need my overridden .then method to still be called to ensure the ref counting works. The minimum requirement for that is to have p.constructor !== Promise - if you satisfy that, it doesn't matter what the prototype is. (I personally found the decision to not use prototypes odd, but it is what it is.) The classes set this implicitly on the prototype, but setting it explicitly on the instance also works.
This is to name a few subtleties. The code isn't self-explanatory and should've been documented with an entire essay, but I'll take the fall on that (I wrote it). But anyways, welcome to the worlds of duck typing, advanced subtyping, and subtle behaviors.
Dropping in to call attention to this much needed feature. There are now multiple user land implementations that are not compatible. See React and Bun.
Both Bun and React have added support for this for performance reasons, waiting a microtask to wait for the value is prohibitive.
To be clear, these use cases require the value to be synchronously available.
One thing that Atomics has going for it is that it's much easier to spot in code review and for machines to warn on in lint rules.
I do think that this is a feature that could be misused to create bad APIs but agree with you in that it's not a very good reason for not adding the feature on its own - there are many ways to create bad APIs.
With both of these in mind, I'd recommend not adding it as a property or method on the promise prototype. Something like Promise.peek(p) would make much more sense.
How can a framework like React possibly expose information that isn't available? Do you have a link to this react mis-feature? Edit: Found use in Suspense, see next message.
Bun being a host with direct engine access has the ability to expose an API for this, but I also cannot find information about it. Any links? I just found Bun.peek(), which mentions "you probably shouldn’t use it".
To be clear, I'm pretty confident the committee will never expose this feature in the language itself. The potential for mis-usage is a good reason to prevent it being included. I also still haven't been convinced that any use case that claims needing this cannot be solved another way (and I have had to jump through hoops myself to deal with this limitation).
The React "peek" provided by use is only in the context of a Suspense, which brings a lot of interrupt / re-execution magic to simulate basically what await or yield does. I don't see this as fundamentally changing the semantics of promise handling. The fact is this is a pure userland implementation requiring a surrounding executor, and the use case doesn't seem to be performance per se, but different ergonomics to handle what gets rendered while resources are still pending.
I don't think that a native way to peek into the status of promises would change any of these ergonomics provided by use & suspense. It might allow their internal implementation to shortcut some of their re-execution when it first discovers a promise.
If you pass React a promise without value/status fields set, React will set them so that when you pass the same promise again React can use the more optimised path. But this still leaves room for de-opts when the promise is first seen.
React could use async/await and user-blocking tasks to detect whether async reaction code has settled. It has chosen a different path, but I maintain that the user experience would be the same, and not require promise introspection.
I do not deny that scheduling promise jobs and async function suspensions come with some overhead, but whether you nest synchronous calls or sequentially execute promise jobs does not fundamentally change the fact you can perform all these operations during a same render task.