srmagura / real-cancellable-promise

Cancellable promise library for JavaScript and TypeScript.
https://srmagura.github.io/real-cancellable-promise
MIT License
33 stars 2 forks source link

Add `raceAndCancel` helper function for structured concurrency #11

Closed ondrej-stanek-ozobot closed 11 months ago

ondrej-stanek-ozobot commented 11 months ago

Motivation:

The core concept of Structured concurrency [1] requires that "all spawned threads have completed before exit". The classic implementation of Promise.race doesn't meet this requirement, as the returned promise finishes sooner than any of the other promises and the other promises are kept running until completion. Therefore, the Promise.race violates the basic principle of structured concurrency when used as a building block for combining parallel tasks.

We propose a simple wrapper for the Promise.race that ensures all remaining promises are automatically cancelled upon completion of the first one. We name the wrapper raceAndCancel, however, we are open to any other naming suggestions.

Example of use:

    const result = await raceAndCancel([
        someLongOperation(),
        CancellablePromise.delay(timeout)
    ])

[1] https://en.wikipedia.org/wiki/Structured_concurrency

ondrej-stanek-ozobot commented 11 months ago

This PR is a draft, if the concept of raceAndCancel is welcomed by the maintainers, we are happy to bring it to production quality, add tests, etc..

ondrej-stanek-ozobot commented 11 months ago

This is an example how a timeout for a (cancellable) promise can be implemented using raceAndCancel:

/**
 * Guards the duration of a cancellable promise resolution with a timeout.
 * If promise resolves before the timeout, its result is returned.
 * Otherwise, the promise is cancelled and timeout exception is thrown.
 * 
 * @param promise promise to be guarded with timeout, has to implement the CancellablePromise contract
 * @param timeout_ms
 * @returns value of the resolved promise
 */
export function timeoutGuard<T extends CancellablePromise<unknown>>(
  promise: T,
  timeout_ms: number
) {

  // promise that never resolves, but throws a timeout error after `timeout_ms` lapsed
  const timeout = CancellablePromise.delay(timeout_ms).then(
    () => { throw new Error("Timeout") }
  )

  return raceAndCancel([ promise, timeout ])
}
srmagura commented 11 months ago

Hey @ondrej-stanek-ozobot, nice work. My only concern is, I am not sure how frequently this new function would be used, and I would prefer to keep the API surface area of this library relatively small.

Is there a reason why raceAndCancel needs to be part of the real-cancellable-promise source code? It seems like this function is straightforward to implement in userland.

Some ideas for how we could proceed:

Thanks 😀

ondrej-stanek-ozobot commented 11 months ago

I totally understand, no problem.

We are using this (and other) helper functions quite extensively in our codebase, and the real-cancellable-promise became a cornerstone on which we build the structured concurrency patterns. Thanks for this great project!

We are thinking about publishing the helpers for structured concurrency. If the intention is to keep real-cancellable-promise relatively small, perhaps we could publish the helpers as a separate package that tightly depends on real-cancellable-promise?

Because the real-cancellable-promise is such a great fit, I wanted to ask if you have interest in structured concurrency and related software patterns?

srmagura commented 11 months ago

Publishing your structured concurrency helper functions as a new npm package is a cool idea! Let me know if you end up doing that.