Yes, you are not supposed to use promises for that. A Promise represents the future value of the request - but the request itself can't be represented solely by a promise, because "the request" encapsulates the work itself, which a Promise does not.
In other words, you want an entirely distinct primitive - let's call it a Task - that produces a Promise but is not, itself, a Promise.
And: ifYouGetSomeData( thenDoThis: function )
Guaranteed thenDoThis() will be called exactly 1 or 0 times.
Maybe, but... am I really not supposed to use Promises?
If I build my own flow control using the above functions, it behaves identically to exposed Promises, but loses async / await.
EPromise.resolve(1).then(v => v !== 1
|| EPromise.reject('should not throw 1')); // => throw "Uncaught (in promise): should not throw 1"
EPromise.resolve(1).then(v => v !== 1
|| console.log('should not throw 1') + EPromise.abort()); // => "should not throw 1"
Reversely, we can also handle some error in a callback method and rethrow some when they are not unhandled:
EPromise.reject(-1).then(() => {}, v => v !== -1
? console.log('handle when not rejected with -1')
: EPromise.rethrow()); // => throw "Uncaught (in promise): -1"
Thanks for the code snippet @0X-JonMichaelGalindo - I'm going to try and parse it to the best of my abilities and offer an alternative solution. Obviously, I don't know a whole lot about your code besides what you gave, so it could be that what you did really is the best solution, and what I'm presenting is just garbage. But, it's another way to think about the problem.
// util.js
// I often use a little event-emitter helper like this in my code
export function createEventEmitter() {
const listeners = []
return {
subscribe(fn) {
listeners.push(fn)
let unsubscribed = false
return function unsubscribe() {
if (unsubscribed) return
const index = listeners.indexOf(fn)
listeners.splice(index, 1)
unsubscribed = true
}
},
trigger(...args) {
return listeners.map(fn => fn(...args))
}
}
}
// yourStuff.js
import { createEventEmitter } from './util.js'
const onSolutionFound = createEventEmitter()
const onLogout = createEventEmitter()
const onConnectionClose = createEventEmitter()
websocket.send( 'some request' );
api.somePromise = new Promise((resolve_, reject_) => {
let unsubscribeHandlers = []
const unsubscribeAll = () => unsubscribeHandlers.forEach(fn => fn())
const resolve = (...args) => { unsubscribeAll(); resolve_(...args) }
const reject = (...args) => { unsubscribeAll(); reject_(...args) }
const onMessage = () => {
if( isSomething( message ) ) resolve( /* If anyone cares, here's the answer. */ )
if( isOther( message ) ) reject( /* If anyone cares, abort this. */ )
}
websocket.addEventListener('message', onMessage)
const unsubscribeHandlers = [
() => websocket.removeEventListener('message', onMessage)
onSolutionFound.subscribe(({ success }) => {
if (success) {
resolve( /* My answer might be ignored. Use if desired. */ )
} else {
reject( /* If this is still alive, it needs to abort. */ )
}
}),
onLogout.subscribe(() => {
reject( /*...*/ )
}),
onConnectionClose.subscribe(() => {
reject( /* ... */ )
})
]
})
//...
websocket.onmessage = message => {
//...
}
//...
websocket.onclose = () => {
onConnectionClose.trigger()
}
//...
api.logout = () => {
//...
onLogout.trigger()
}
//...
// code that must only execute if nothing cancels somePromise
api.somePromise.then( /*...*/ );
// independent code paths racing to provide a solution or reason for cancellation:
if( /*...*/ ) {
onSolutionFound.trigger({ success: true })
}
//...
if( /*...*/ ) {
onSolutionFound.trigger({ success: false })
}
You'll notice that I'm finding different ways to move the promise-related logic together, instead of having it spread out throughout the entire module. This (perhaps) makes it easier to figure out what will cause that promise to resolve and reject (you just have to look at once place), and (perhaps) makes the code easier to reason about.
On a different note, here's an article that got shared on these forms at a different time (I can't remember who shared it), that I found very interesting. It's about different forms of promise cancelation, and why cancel tokens really are the most robust form. (Many other versions have different issues in different scenarios)
Thanks. That is a solid technique, and very useful.
"figure out what will cause that promise to resolve and reject" is not the development approach I've personally decided to rely on.
At this point, I am convinced that Promises should not be exposed by default, but I am also convinced that there is an opportunity for JS to offer some API that addresses cases like mine.
I will let this idea go (still using it personally for now). Maybe in the future, with more experience and development, I can offer something more helpful.
I have to deal with interdependent, parallel, distributed, redundant processes. I have to write code that:
Knows what kind of results it should try to achieve
Needs to initiate and await network events to achieve its results
Doesn't know when or if it will be executed
Doesn't know what kind of data to expect (hints)
Doesn't know under what conditions it needs to halt
Doesn't know how/whether its results will/should be used
My approach is about fail-safety, recovery, and race-based, latest-based, or priority-based guarantees.
Some lessons learned:
Interdependent code modules will never start an unknown-length loop
Interdependent code modules will never time-schedule future code execution
Interdependent code modules will never accumulate state data over time
Thanks for taking the time to help me understand Promises better!