Remix.run Logo
kaoD a day ago

Just to clarify because the message might have been lost: I'm not saying you're wrong! I'm saying you're right, and...

Quoting a comment from the issue you linked:

> This is not specific to Promise.race, but for any callback attached a promise that will never be resolved like this:

  x = new Promise(() => {});
  for (let i = 0; i < 10e5 ; i++) {
    x.then(() => {});
  }
My point is if you do something like this (see below) instead, the same issue is still there and cannot be resolved just by using `Unpromise.race` because the underlying issue is promise cancellation:

  // Use this in the `race` instead
  // Will also leak memory even with `Unpromise.race`
  const interruptPromiseAndLog = () =>
    interruptPromise()
      .then(() => console.log('SIGINT'))
`Unpromise.race` only helps with its internal `then` so it will only help if the promise you're using has no inner `then` or `await` after the non-progressing point.

This is not a theoretical issue. This code happens all the time naturally, including in library code that you have no control over.

So you have to proxy this promise too... but again this only partially solves the issue because you'd have to promise every single promise that might ever be created, including those you have no control over (in library code) and therefore cannot proxy yourself.

And the ergonomics are terrible. If you do this, you have to proxy and propagate unsubscription to both `then`s:

  const interruptPromiseAndLog = () =>
    interruptPromise()
      // How do you unsubscribe this one
      .then(() => console.log('SIGINT'))
      // ...even if you can easily proxy this one?
      .then(() => console.log('REALLY SIGINT'))
Which can easily happen in await points too:

  const interruptPromiseAndLog = async () => {
    console.log('Waiting for SIGINT')

    // You have to proxy and somehow propagate unsubscription to this one too... how!?
    await interruptPromise()
    
    console.log('SIGINT')
  }
Since this is just sugar for:

  const interruptPromiseAndLog = () => {
    console.log('Waiting for SIGINT')

    return interruptPromise()
      // Needs unsubscription forwarded here
      .then(() => console.log('SIGINT'))
  }
Which can quickly get out of hand with multiple await points (i.e. many `then`s).

Hence why I say the underlying issue is overall promise cancellation and how you actually have no ownership of promises in JS userspace, only of their completion handles (the event loop is the actual promise owner) which do nothing when going out of scope (only the handle is GC'd but the promise stays alive in the event loop).