The code is so m.request(...).then(...)
waits for its subsequent then
chain to resolve before implicitly redrawing. It's a wrapper, not a monkey patch, as it only overrides properties in a newly created (and trusted) Promise
. The current code is considerably simpler (bulk of it in mithril.js/request/request.js at 274cb2fff2a0f870a7215f245be26c11bf96ae92 · MithrilJS/mithril.js · GitHub) but the same concerns remain.
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 1 2 3 4 5 6), 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.
Also, your code could be improved:
- You don't need to return a new wrapped promise. You can just return the original and get equivalent behavior.
- You propagated your state wrong in
resolveSync
. You can't assume it's resolved, because it could be a pending promise.
- No need to freeze if you return fresh objects each time. (This is actually faster if your objects are frequently created.)
- A better API IMO mirrors
Promise.allSettled
's resolution values.
// sync-promise-library.js
const states = new WeakMap();
const wrapMissing = (promise) => {
states.set(promise, { status: 'pending' });
promise.then(
(value) => { states.set(promise, { status: 'fulfilled', value }); },
(reason) => { states.set(promise, { status: 'rejected', reason }); },
);
};
export const resolve = (value) => {
// A type check is sufficient. No need to
// inspect prototypes for just a hash map
// presence test, but this does avoid having
// exceptions thrown.
if (value !== null && typeof value === 'object') {
if (states.has(value)) return value;
}
const promise = Promise.resolve(value));
wrapMissing(promise);
return promise;
};
export const wrap = (promise) => {
if (!states.has(promise)) wrapMissing(promise);
return promise;
};
export const getState = (promise) => {
const state = states.get(promise);
if (state) return { ...state };
wrapMissing(promise);
return {status: 'pending"}
};
1 Like