
japgolly.scalajs.react.callback.AsyncCallback.scala Maven / Gradle / Ivy
package japgolly.scalajs.react.callback
import japgolly.scalajs.react.util.JsUtil
import japgolly.scalajs.react.util.Util.{catchAll, identityFn}
import java.time.Duration
import scala.collection.BuildFrom
import scala.concurrent.duration.{FiniteDuration, MILLISECONDS}
import scala.concurrent.{ExecutionContext, Future}
import scala.language.`3.0`
import scala.scalajs.js
import scala.scalajs.js.timers
import scala.util.{Failure, Success, Try}
object AsyncCallback {
type UnderlyingRepr[+A] = State => (Try[A] => Callback) => Callback
final class State {
var cancelled = false
val cancelCallbacks = new js.Array[js.UndefOr[UnderlyingRepr[Unit]]]
def onCancel(f: AsyncCallback[Unit]): Int = {
cancelCallbacks.addOne(f.underlyingRepr)
cancelCallbacks.length - 1
}
inline def unCancel(i: Int): Unit =
cancelCallbacks(i) = ()
def cancelCallback: Option[Callback] = {
val it = cancelCallbacks.iterator.filter(_.isDefined)
Option.when(it.nonEmpty) {
it.map(u => new AsyncCallback(u.get).toCallback.reset).reduce(_ << _)
}
}
def cancelably(c: => Callback): Callback =
Callback.suspend(Callback.unless(cancelled)(c))
}
def cancel: AsyncCallback[Unit] =
new AsyncCallback[Unit](s => {
s.cancelled = true
unit.underlyingRepr(s)
})
private[AsyncCallback] val newState: CallbackTo[State] =
CallbackTo(new State)
private[AsyncCallback] val defaultCompleteWith: Try[Any] => Callback =
_ => Callback.empty
def apply[A](f: (Try[A] => Callback) => Callback): AsyncCallback[A] =
new AsyncCallback(_ => f)
def init[A, B](f: (Try[B] => Callback) => CallbackTo[A]): CallbackTo[(A, AsyncCallback[B])] =
promise[B].flatMap { case (ac, c) =>
f(c).map { a =>
(a, ac)
}
}
/** Create an AsyncCallback and separately provide the completion function.
*
* This is like Scala's promise, not the JS promise which is more like Scala's Future.
*/
def promise[A]: CallbackTo[(AsyncCallback[A], Try[A] => Callback)] =
for {
p <- SyncPromise[A]
} yield (AsyncCallback(p.onComplete), p.complete)
def first[A](f: (Try[A] => Callback) => Callback): AsyncCallback[A] =
firstS(_ => f)
private[react] def firstS[A](f: State => (Try[A] => Callback) => Callback): AsyncCallback[A] =
new AsyncCallback(s => g => CallbackTo {
var first = true
f(s)(ea => Callback.when(first)(Callback{first = false} >> g(ea)))
}.flatten)
/** AsyncCallback that never completes. */
def never[A]: AsyncCallback[A] =
apply(_ => Callback.empty)
@deprecated("Use AsyncCallback.delay", "1.7.0")
def point[A](a: => A): AsyncCallback[A] =
delay(a)
def delay[A](a: => A): AsyncCallback[A] =
AsyncCallback(f => CallbackTo(catchAll(a)).flatMap(f))
def pure[A](a: A): AsyncCallback[A] =
const(Success(a))
def throwException[A](t: => Throwable): AsyncCallback[A] =
const {
try
Failure(t)
catch {
case t2: Throwable => Failure(t2)
}
}
def throwExceptionWhenDefined(o: => Option[Throwable]): AsyncCallback[Unit] =
suspend {
o match {
case None => unit
case Some(t) => throwException(t)
}
}
inline def const[A](t: Try[A]): AsyncCallback[A] =
AsyncCallback(_(t))
val unit: AsyncCallback[Unit] =
pure(())
/** Callback that isn't created until the first time it is used, after which it is reused. */
def lazily[A](f: => AsyncCallback[A]): AsyncCallback[A] = {
lazy val g = f
suspend(g)
}
/** Callback that is recreated each time it is used.
*
* https://en.wikipedia.org/wiki/Evaluation_strategy#Call_by_name
*/
def suspend[A](f: => AsyncCallback[A]): AsyncCallback[A] =
delay(f).flatten
/** Callback that is recreated each time it is used.
*
* https://en.wikipedia.org/wiki/Evaluation_strategy#Call_by_name
*/
@deprecated("Use AsyncCallback.suspend", "2.0.0")
def byName[A](f: => AsyncCallback[A]): AsyncCallback[A] =
suspend(f)
@deprecated("Use c.asAsyncCallback", "")
def fromCallback[A](c: CallbackTo[A]): AsyncCallback[A] =
c.asAsyncCallback
/** Traverse stdlib T over AsyncCallback.
* Distribute AsyncCallback over stdlib T.
*/
def traverse[T[X] <: Iterable[X], A, B](ta: => T[A])(f: A => AsyncCallback[B])(using cbf: BuildFrom[T[A], B, T[B]]): AsyncCallback[T[B]] =
AsyncCallback.suspend {
val _ta = ta
val as = _ta.iterator.to(Vector)
if (as.isEmpty)
AsyncCallback.pure(cbf.newBuilder(_ta).result())
else {
val discard = (_: Any, _: Any) => ()
val bs = new js.Array[B](as.length)
as
.indices
.iterator
.map(i => f(as(i)).map(b => bs(i) = b))
.reduce(_.zipWith(_)(discard))
.map(_ => cbf.fromSpecific(_ta)(bs))
}
}
/** Sequence stdlib T over AsyncCallback.
* Co-sequence AsyncCallback over stdlib T.
*/
def sequence[T[X] <: Iterable[X], A](tca: => T[AsyncCallback[A]])(using cbf: BuildFrom[T[AsyncCallback[A]], A, T[A]]): AsyncCallback[T[A]] =
traverse(tca)(identityFn)
/** Traverse Option over AsyncCallback.
* Distribute AsyncCallback over Option.
*/
def traverseOption[A, B](oa: => Option[A])(f: A => AsyncCallback[B]): AsyncCallback[Option[B]] =
AsyncCallback.delay(oa).flatMap {
case Some(a) => f(a).map(Some(_))
case None => AsyncCallback.pure(None)
}
/** Sequence Option over AsyncCallback.
* Co-sequence AsyncCallback over Option.
*/
def sequenceOption[A](oca: => Option[AsyncCallback[A]]): AsyncCallback[Option[A]] =
traverseOption(oca)(identityFn)
/** Same as [[traverse()]] except avoids combining return values. */
def traverse_[T[X] <: Iterable[X], A, B](ta: => T[A])(f: A => AsyncCallback[B]): AsyncCallback[Unit] =
AsyncCallback.suspend {
val as = new js.Array[A]
for (a <- ta.iterator)
as.push(a)
as.length match {
case 0 => AsyncCallback.unit
case 1 => AsyncCallback.suspend(f(as(0))).void
case n =>
var error = Option.empty[Throwable]
val latch = countDownLatch(n).runNow()
def onTaskComplete(r: Try[B]): Callback =
Callback {
r match {
case Success(_) =>
case Failure(e) => error = Some(e)
}
} >> latch.countDown
for (a <- as)
AsyncCallback.suspend(f(a))
.attemptTry
.flatMapSync(onTaskComplete)
.runNow()
latch.await >> throwExceptionWhenDefined(error)
}
}
/** Same as [[sequence()]] except avoids combining return values. */
def sequence_[T[X] <: Iterable[X], A](tca: => T[AsyncCallback[A]]): AsyncCallback[Unit] =
traverse_(tca)(identityFn)
/** Same as [[traverseOption()]] except avoids combining return values. */
def traverseOption_[A, B](oa: => Option[A])(f: A => AsyncCallback[B]): AsyncCallback[Unit] =
AsyncCallback.delay(oa).flatMap {
case Some(a) => f(a).void
case None => AsyncCallback.unit
}
/** Same as [[sequenceOption()]] except avoids combining return values. */
def sequenceOption_[A](oca: => Option[AsyncCallback[A]]): AsyncCallback[Unit] =
traverseOption_(oca)(identityFn)
def fromFuture[A](fa: => Future[A])(using ec: ExecutionContext): AsyncCallback[A] =
AsyncCallback(f => Callback {
val future = fa
future.value match {
case Some(value) => f(value).runNow()
case None => future.onComplete(f(_).runNow())
}
})
def fromCallbackToFuture[A](c: CallbackTo[Future[A]])(using ec: ExecutionContext): AsyncCallback[A] =
c.asAsyncCallback.flatMap(fromFuture(_))
def fromJsPromise[A](pa: => js.Thenable[A]): AsyncCallback[A] =
AsyncCallback(f => Callback {
JsUtil.runPromiseAsync(pa)(f(_).toJsFn)
})
def fromCallbackToJsPromise[A](c: CallbackTo[js.Promise[A]]): AsyncCallback[A] =
c.asAsyncCallback.flatMap(fromJsPromise(_))
/** Not literally tail-recursive because AsyncCallback is continuation-based, but this utility in this shape may still
* be useful.
*/
def tailrec[A, B](a: A)(f: A => AsyncCallback[Either[A, B]]): AsyncCallback[B] =
f(a).flatMap {
case Left(a2) => tailrec(a2)(f)
case Right(b) => pure(b)
}
private lazy val tryUnit: Try[Unit] =
Try(())
def viaCallback(onCompletion: Callback => Callback): AsyncCallback[Unit] =
for {
p <- SyncPromise[Unit].asAsyncCallback
_ <- onCompletion(p.complete(tryUnit)).asAsyncCallback
_ <- AsyncCallback(p.onComplete)
} yield ()
private[react] inline def debounce[A](inline delayMs: Long, inline self: AsyncCallback[A])(implicit timer: Timer): AsyncCallback[A] =
if (delayMs <= 0)
self
else {
val f = _debounce[Unit, A](delayMs, _ => self)
suspend(f(()))
}
private inline def _debounce[A, B](inline delayMs: Long, inline f: A => AsyncCallback[B])(using timer: Timer): A => AsyncCallback[B] = {
var prev = Option.empty[timer.Handle]
var invocationNum = 0
a => {
invocationNum += 1
val (promise, completePromise) = AsyncCallback.promise[B].runNow()
def run(): Unit = {
prev = None
val curInvocationNum = invocationNum
f(a).tap { b =>
if (invocationNum == curInvocationNum)
completePromise(Success(b)).runNow()
}.toCallback.runNow()
}
prev.foreach(timer.cancel)
prev = Some(timer.delay(delayMs)(run()))
promise
}
}
/** Creates an debounce boundary.
*
* Save it as a `val` somewhere because it relies on internal state that must be reused.
*/
inline def debounce(delay: Duration): AsyncCallback[Unit] =
unit.debounce(delay)
/** Creates an debounce boundary.
*
* Save it as a `val` somewhere because it relies on internal state that must be reused.
*/
inline def debounce(delay: FiniteDuration): AsyncCallback[Unit] =
unit.debounce(delay)
/** Creates an debounce boundary.
*
* Save it as a `val` somewhere because it relies on internal state that must be reused.
*/
inline def debounceMs(delayMs: Long): AsyncCallback[Unit] =
unit.debounceMs(delayMs)
def awaitAll(as: AsyncCallback[Any]*): AsyncCallback[Unit] =
if (as.isEmpty)
unit
else
sequence_(as)
// ===================================================================================================================
final case class Forked[+A](await: AsyncCallback[A], isComplete: CallbackTo[Boolean])
// ===================================================================================================================
final class Barrier(val await: AsyncCallback[Unit], completePromise: Callback) {
private var _complete = false
def complete: Callback =
completePromise.finallyRun(Callback { _complete = true })
def isComplete: CallbackTo[Boolean] =
CallbackTo(_complete)
@deprecated("Use .await", "1.7.7")
inline def waitForCompletion: AsyncCallback[Unit] =
await
}
/** A synchronisation aid that allows you to wait for another async process to complete. */
lazy val barrier: CallbackTo[Barrier] =
promise[Unit].map { case (promise, complete) =>
new Barrier(promise, complete(tryUnit))
}
// ===================================================================================================================
final class CountDownLatch(count: Int, barrier: Barrier) {
private var _pending = count.max(0)
/** Decrements the count of the latch, releasing all waiting computations if the count reaches zero. */
val countDown: Callback =
Callback {
if (_pending > 0) {
_pending -= 1
if (_pending == 0) {
barrier.complete.runNow()
}
}
}
inline def await: AsyncCallback[Unit] =
barrier.await
inline def isComplete: CallbackTo[Boolean] =
barrier.isComplete
def pending: CallbackTo[Int] =
CallbackTo(_pending)
}
/** A synchronization aid that allows you to wait until a set of async processes completes. */
def countDownLatch(count: Int): CallbackTo[CountDownLatch] =
for {
b <- barrier
_ <- b.complete.when_(count <= 0)
} yield new CountDownLatch(count, b)
// ===================================================================================================================
final class Mutex private[AsyncCallback]() {
private var mutex: Option[Barrier] =
None
private val release: Callback =
CallbackTo {
val old = mutex
mutex = None
old
}.flatMap(Callback.traverseOption(_)(_.complete))
/** Wrap a [[AsyncCallback]] so that it executes in the mutex.
*
* Note: THIS IS NOT RE-ENTRANT. Calling this from within the mutex will block.
*/
def apply[A](ac: AsyncCallback[A]): AsyncCallback[A] =
suspend {
mutex match {
case None =>
// Mutex empty
val b = barrier.runNow()
mutex = Some(b)
ac.finallyRunSync(release)
case Some(b) =>
// Mutex in use
b.await >> apply(ac)
}
}
}
/** Creates a new (non-reentrant) mutex. */
def mutex: CallbackTo[Mutex] =
CallbackTo(new Mutex)
// ===================================================================================================================
final class ReadWriteMutex private[AsyncCallback]() {
// Whether it's a read or write mutex is determined by readers being > 0 or not
private var mutex: Option[AsyncCallback.Barrier] =
None
private var readers =
0
private val releaseMutex: Callback =
CallbackTo {
if (readers == 0) {
val old = mutex
mutex = None
old
} else
None
}.flatMap(Callback.traverseOption(_)(_.complete))
private val releaseReader: Callback =
CallbackTo {
readers -= 1
} >> releaseMutex
/** Wrap a [[AsyncCallback]] so that it executes in the write-mutex.
* There can only be one writer active at one time.
*
* Note: THIS IS NOT RE-ENTRANT. Calling this from within the read or write mutex will block.
*/
def write[A](ac: AsyncCallback[A]): AsyncCallback[A] =
AsyncCallback.suspend {
mutex match {
case None =>
// Mutex empty
val b = AsyncCallback.barrier.runNow()
mutex = Some(b)
ac.finallyRunSync(releaseMutex)
case Some(b) =>
// Mutex in use
b.await >> write(ac)
}
}
/** Wrap a [[AsyncCallback]] so that it executes in the read-mutex.
* There can be many readers active at one time.
*
* Note: Calling this from within the write-mutex will block.
*/
def read[A](ac: AsyncCallback[A]): AsyncCallback[A] =
AsyncCallback.suspend {
mutex match {
case None =>
// Mutex empty
val b = AsyncCallback.barrier.runNow()
mutex = Some(b)
assert(readers == 0)
readers = 1
ac.finallyRunSync(releaseReader)
case Some(b) =>
if (readers > 0) {
// Read-mutex in use
readers += 1
ac.finallyRunSync(releaseReader)
} else {
// Write-mutex in use
b.await >> read(ac)
}
}
}
}
/** Creates a new (non-reentrant) read/write mutex. */
def readWriteMutex: CallbackTo[ReadWriteMutex] =
CallbackTo(new ReadWriteMutex)
// ===================================================================================================================
final class Ref[A] private[AsyncCallback](atomicReads: Boolean, atomicWrites: Boolean) {
private val mutex = readWriteMutex.runNow()
private val initialised = barrier.runNow()
private var _value: A = _
private val markInitialised =
initialised.complete.asAsyncCallback
/** If this hasn't been set yet, it will block until it is set. */
val get: AsyncCallback[A] = {
var readValue: AsyncCallback[A] =
AsyncCallback.delay(_value)
if (atomicReads)
readValue = mutex.read(readValue)
initialised.await >> readValue
}
/** Synchronously return whatever value is currently stored. (Ignores atomicity). */
lazy val getIfAvailable: CallbackTo[Option[A]] =
initialised.isComplete.map {
case true => Some(_value)
case false => None
}
private def inWriteMutex[B](a: AsyncCallback[B]): AsyncCallback[B] =
if (atomicWrites)
mutex.write(a)
else
a
// This must only be called within the write mutex
private def setWithinMutex(c: AsyncCallback[A]): AsyncCallback[Unit] =
for {
a <- c
_ <- AsyncCallback.delay { _value = a }
_ <- markInitialised
} yield ()
def set(a: => A): AsyncCallback[Unit] =
setAsync(AsyncCallback.delay(a))
def setSync(c: CallbackTo[A]): AsyncCallback[Unit] =
setAsync(c.asAsyncCallback)
def setAsync(c: AsyncCallback[A]): AsyncCallback[Unit] =
inWriteMutex(setWithinMutex(c))
/** Returns whether or not the value was set. */
def setIfUnset(a: => A): AsyncCallback[Boolean] =
setIfUnsetAsync(AsyncCallback.delay(a))
/** Returns whether or not the value was set. */
def setIfUnsetSync(c: CallbackTo[A]): AsyncCallback[Boolean] =
setIfUnsetAsync(c.asAsyncCallback)
/** Returns whether or not the value was set. */
def setIfUnsetAsync(c: AsyncCallback[A]): AsyncCallback[Boolean] =
initialised.isComplete.asAsyncCallback.flatMap {
case true => AsyncCallback.pure(false)
case false =>
inWriteMutex {
AsyncCallback.suspend {
if (initialised.isComplete.runNow())
AsyncCallback.pure(false)
else
setWithinMutex(c).ret(true)
}
}
}
}
inline def ref[A]: CallbackTo[Ref[A]] =
ref()
def ref[A](allowStaleReads: Boolean = false,
atomicWrites : Boolean = true): CallbackTo[Ref[A]] =
CallbackTo(new Ref(
atomicReads = atomicWrites && !allowStaleReads,
atomicWrites = atomicWrites,
))
// ===================================================================================================================
extension [A, B](self: AsyncCallback[A => B]) {
/** Function distribution. See `AsyncCallback.liftTraverse(f).id` for the dual. */
def distFn: A => AsyncCallback[B] =
a => self.map(_(a))
}
}
// █████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
/** Pure asynchronous callback.
*
* You can think of this as being similar to using `Future` - you can use it in for-comprehensions the same way -
* except `AsyncCallback` is pure and doesn't need an `ExecutionContext`.
*
* When combining instances, it's good to know which methods are sequential and which are parallel
* (or at least concurrent).
*
* The following methods are sequential:
* - [[>>=()]] / [[flatMap()]]
* - [[>>()]] & [[<<()]]
* - [[flatTap()]]
*
* The following methods are effectively parallel:
* - [[*>()]] & [[<*()]]
* - [[race()]]
* - [[zip()]] & [[zipWith()]]
* - `AsyncCallback.traverse` et al
*
* In order to actually run this, or get it into a shape in which in can be run, use one of the following:
* - [[toCallback]] <-- most common
* - [[asCallbackToFuture]]
* - [[asCallbackToJsPromise]]
* - [[unsafeToFuture()]]
* - [[unsafeToJsPromise()]]
*
* A good example is the [Ajax 2 demo](https://japgolly.github.io/scalajs-react/#examples/ajax-2).
*
* @tparam A The type of data asynchronously produced on success.
*/
final class AsyncCallback[+A] private[AsyncCallback] (val underlyingRepr: AsyncCallback.UnderlyingRepr[A]) extends AnyVal { self =>
def completeWith(f: Try[A] => Callback): Callback =
AsyncCallback.newState.flatMap(underlyingRepr(_)(f))
def map[B](f: A => B): AsyncCallback[B] =
flatMap(a => AsyncCallback.pure(f(a)))
/** Alias for `map`. */
inline def |>[B](f: A => B): AsyncCallback[B] =
map(f)
def flatMap[B](f: A => AsyncCallback[B]): AsyncCallback[B] =
new AsyncCallback(s => g =>
s.cancelably {
underlyingRepr(s) {
case Success(a) =>
catchAll(f(a)) match {
case Success(next) => s.cancelably(next.underlyingRepr(s)(g))
case Failure(e) => g(Failure(e))
}
case Failure(e) => g(Failure(e))
}
}
)
/** Alias for `flatMap`. */
inline def >>=[B](inline g: A => AsyncCallback[B]): AsyncCallback[B] =
flatMap(g)
def flatten[B](using ev: A => AsyncCallback[B]): AsyncCallback[B] =
flatMap(ev)
/** Sequence the argument a callback to run after this, discarding any value produced by this. */
def >>[B](runNext: AsyncCallback[B]): AsyncCallback[B] =
flatMap(_ => runNext)
/** Sequence a callback to run before this, discarding any value produced by it. */
inline def <<[B](runBefore: AsyncCallback[B]): AsyncCallback[A] =
runBefore >> this
/** Convenient version of `<<` that accepts an Option */
def <[B](prev: Option[AsyncCallback[B]]): AsyncCallback[A] =
prev.fold(this)(_ >> this)
/** When the callback result becomes available, perform a given side-effect with it. */
def tap(t: A => Any): AsyncCallback[A] =
flatTap(a => AsyncCallback.delay(t(a)))
/** Alias for `tap`. */
inline def <|(t: A => Any): AsyncCallback[A] =
tap(t)
def flatTap[B](t: A => AsyncCallback[B]): AsyncCallback[A] =
for {
a <- this
_ <- t(a)
} yield a
def zip[B](that: AsyncCallback[B]): AsyncCallback[(A, B)] =
zipWith(that)((_, _))
def zipWith[B, C](that: AsyncCallback[B])(f: (A, B) => C): AsyncCallback[C] =
new AsyncCallback(s => cc => s.cancelably(CallbackTo {
var ra: Option[Try[A]] = None
var rb: Option[Try[B]] = None
var r: Option[Try[C]] = None
val respond = Callback {
if (r.isEmpty) {
(ra, rb) match {
case (Some(Success(a)), Some(Success(b))) => r = Some(catchAll(f(a, b)))
case (Some(Failure(e)), _ ) => r = Some(Failure(e))
case (_ , Some(Failure(e))) => r = Some(Failure(e))
case (Some(Success(_)), None )
| (None , Some(Success(_)))
| (None , None ) => ()
}
r.foreach(cc(_).runNow())
}
}
this.underlyingRepr(s)(e => Callback {ra = Some(e)} >> respond) >>
that.underlyingRepr(s)(e => Callback {rb = Some(e)} >> respond)
}.flatten))
/** Start both this and the given callback at once and when both have completed successfully,
* discard the value produced by this.
*/
def *>[B](next: AsyncCallback[B]): AsyncCallback[B] =
zipWith(next)((_, b) => b)
/** Start both this and the given callback at once and when both have completed successfully,
* discard the value produced by the given callback.
*/
def <*[B](next: AsyncCallback[B]): AsyncCallback[A] =
zipWith(next)((a, _) => a)
/** Discard the callback's return value, return a given value instead.
*
* `ret`, short for `return`.
*/
def ret[B](b: B): AsyncCallback[B] =
map(_ => b)
/** Discard the value produced by this callback. */
def void: AsyncCallback[Unit] =
map(_ => ())
/** Discard the value produced by this callback.
*
* This method allows you to be explicit about the type you're discarding (which may change in future).
*/
inline def voidExplicit[B](using inline ev: A <:< B): AsyncCallback[Unit] =
void
/** Wraps this callback in a try-catch and returns either the result or the exception if one occurs. */
def attempt: AsyncCallback[Either[Throwable, A]] =
new AsyncCallback(s => f => underlyingRepr(s)(e => f(Success(e match {
case Success(a) => Right(a)
case Failure(t) => Left(t)
}))))
def attemptTry: AsyncCallback[Try[A]] =
new AsyncCallback(s => f => underlyingRepr(s)(e => f(Success(e))))
/** If this completes successfully, discard the result.
* If any exception occurs, call `printStackTrace` and continue.
*
* @since 2.0.0
*/
def reset: AsyncCallback[Unit] =
new AsyncCallback(s => f => underlyingRepr(s)(e => f(Success(e match {
case _: Success[A] => ()
case Failure(t) => t.printStackTrace()
}))))
def handleError[AA >: A](f: Throwable => AsyncCallback[AA]): AsyncCallback[AA] =
new AsyncCallback(s => g => underlyingRepr(s) {
case r@ Success(_) => g(r)
case Failure(t) => f(t).underlyingRepr(s)(g)
})
def maybeHandleError[AA >: A](f: PartialFunction[Throwable, AsyncCallback[AA]]): AsyncCallback[AA] =
new AsyncCallback(s => g => underlyingRepr(s) {
case r@ Success(_) => g(r)
case l@ Failure(t) => f.lift(t) match {
case Some(n) => n.underlyingRepr(s)(g)
case None => g(l)
}
})
/** Return a version of this callback that will only execute once, and reuse the result for all
* other invocations.
*/
def memo(): AsyncCallback[A] = {
var result: Option[AsyncCallback[A]] = None
def set(r: AsyncCallback[A]) = {result = Some(r); r}
AsyncCallback.suspend {
result.getOrElse {
val first = attemptTry.flatMap(t => AsyncCallback.suspend(set(AsyncCallback.const(t))))
val promise = first.unsafeToJsPromise()
result getOrElse set(AsyncCallback.fromJsPromise(promise))
}
}
}
/** Conditional execution of this callback.
*
* @param cond The condition required to be `true` for this callback to execute.
* @return `Some` result of the callback executed, else `None`.
*/
def when(cond: => Boolean): AsyncCallback[Option[A]] =
new AsyncCallback(s => f => if (cond) underlyingRepr(s)(ea => f(ea.map(Some(_)))) else f(Success(None)))
/** Conditional execution of this callback.
* Reverse of [[when()]].
*
* @param cond The condition required to be `false` for this callback to execute.
* @return `Some` result of the callback executed, else `None`.
*/
inline def unless(inline cond: Boolean): AsyncCallback[Option[A]] =
when(!cond)
/** Conditional execution of this callback.
* Discards the result.
*
* @param cond The condition required to be `true` for this callback to execute.
*/
inline def when_(inline cond: Boolean): AsyncCallback[Unit] =
when(cond).void
/** Conditional execution of this callback.
* Discards the result.
* Reverse of [[when_()]].
*
* @param cond The condition required to be `false` for the callback to execute.
*/
inline def unless_(inline cond: Boolean): AsyncCallback[Unit] =
when_(!cond)
/** Limits the number of invocations in a given amount of time.
*
* @return Some if invocation was allowed, None if rejected/rate-limited
*/
inline def rateLimit(inline window: Duration): AsyncCallback[Option[A]] =
rateLimitMs(window.toMillis)
/** Limits the number of invocations in a given amount of time.
*
* @return Some if invocation was allowed, None if rejected/rate-limited
*/
inline def rateLimit(inline window: FiniteDuration): AsyncCallback[Option[A]] =
rateLimitMs(window.toMillis)
/** Limits the number of invocations in a given amount of time.
*
* @return Some if invocation was allowed, None if rejected/rate-limited
*/
inline def rateLimit(inline window: Duration, inline maxPerWindow: Int): AsyncCallback[Option[A]] =
rateLimitMs(window.toMillis, maxPerWindow)
/** Limits the number of invocations in a given amount of time.
*
* @return Some if invocation was allowed, None if rejected/rate-limited
*/
inline def rateLimit(inline window: FiniteDuration, inline maxPerWindow: Int): AsyncCallback[Option[A]] =
rateLimitMs(window.toMillis, maxPerWindow)
/** Limits the number of invocations in a given amount of time.
*
* @return Some if invocation was allowed, None if rejected/rate-limited
*/
def rateLimitMs(windowMs: Long, maxPerWindow: Int = 1): AsyncCallback[Option[A]] =
_rateLimitMs(windowMs, maxPerWindow, RateLimit.realClock)
private[react] inline def _rateLimitMs(inline windowMs: Long,
inline maxPerWindow: Int,
inline clock: RateLimit.Clock): AsyncCallback[Option[A]] =
if (windowMs <= 0 || maxPerWindow <= 0)
AsyncCallback.pure(None)
else {
val limited =
RateLimit.fn(
run = (f: Try[A] => Callback) => AsyncCallback.newState.flatMap(underlyingRepr(_)(f)),
windowMs = windowMs,
maxPerWindow = maxPerWindow,
clock = clock,
)
val miss = Try(None)
AsyncCallback { f =>
Callback {
limited(ta => f(ta.map(Some(_)))) match {
case Some(cb) => cb.runNow()
case None => f(miss)
}
}
}
}
/** Creates an debounce boundary over the underlying computation.
*
* Save the result of this as a `val` somewhere because it relies on internal state that must be reused.
*/
inline def debounce(inline delay: Duration): AsyncCallback[A] =
debounceMs(delay.toMillis)
/** Creates an debounce boundary over the underlying computation.
*
* Save the result of this as a `val` somewhere because it relies on internal state that must be reused.
*/
inline def debounce(inline delay: FiniteDuration): AsyncCallback[A] =
debounceMs(delay.toMillis)
/** Creates an debounce boundary over the underlying computation.
*
* Save the result of this as a `val` somewhere because it relies on internal state that must be reused.
*/
def debounceMs(delayMs: Long): AsyncCallback[A] =
AsyncCallback.debounce(delayMs, this)
/** Log to the console before this callback starts, and after it completes.
*
* Does not change the result.
*/
def logAround(message: Any, optionalParams: Any*): AsyncCallback[A] = {
def log(prefix: String) = Callback.log(prefix + message, optionalParams: _*).asAsyncCallback
log("→ Starting: ") *> this <* log(" ← Finished: ")
}
/** Logs the result of this callback as it completes. */
def logResult(msg: A => String): AsyncCallback[A] =
flatTap(a => Callback.log(msg(a)).asAsyncCallback)
/** Logs the result of this callback as it completes.
*
* @param name Prefix to appear the log output.
*/
def logResult(name: String): AsyncCallback[A] =
logResult(a => s"$name: $a")
/** Logs the result of this callback as it completes. */
def logResult: AsyncCallback[A] =
logResult(_.toString)
def delay(dur: Duration): AsyncCallback[A] =
delayMs(dur.toMillis.toDouble)
def delay(dur: FiniteDuration): AsyncCallback[A] =
delayMs(dur.toMillis.toDouble)
def delayMs(milliseconds: Double): AsyncCallback[A] =
if (milliseconds <= 0)
this
else
new AsyncCallback(s => f => Callback {
timers.setTimeout(milliseconds) {
underlyingRepr(s)(f).runNow()
}
})
/** Limit the amount of time you're prepared to wait for a computation.
*
* Note: there's no built-in concept of cancellation here.
* If your procedure doesn't finish in time, this will return `None` when the time limit is reached however, the
* underlying procedure will become orphaned and continue to run in the background until complete.
*/
def timeout(limit: Duration): AsyncCallback[Option[A]] =
timeoutMs(limit.toMillis.toDouble)
/** Limit the amount of time you're prepared to wait for a computation.
*
* Note: there's no built-in concept of cancellation here.
* If your procedure doesn't finish in time, this will return `None` when the time limit is reached however, the
* underlying procedure will become orphaned and continue to run in the background until complete.
*/
def timeout(limit: FiniteDuration): AsyncCallback[Option[A]] =
timeoutMs(limit.toMillis.toDouble)
/** Limit the amount of time you're prepared to wait for a computation.
*
* Note: there's no built-in concept of cancellation here.
* If your procedure doesn't finish in time, this will return `None` when the time limit is reached however, the
* underlying procedure will become orphaned and continue to run in the background until complete.
*/
def timeoutMs(milliseconds: Double): AsyncCallback[Option[A]] =
AsyncCallback.unit.delayMs(milliseconds).race(this).map(_.toOption)
/** Schedule for repeated execution every `dur`. */
inline def setInterval(inline dur: Duration): CallbackTo[Callback.SetIntervalResult] =
toCallback.setInterval(dur)
/** Schedule for repeated execution every `dur`. */
inline def setInterval(inline dur: FiniteDuration): CallbackTo[Callback.SetIntervalResult] =
toCallback.setInterval(dur)
/** Schedule for repeated execution every x milliseconds. */
inline def setIntervalMs(inline milliseconds: Double): CallbackTo[Callback.SetIntervalResult] =
toCallback.setIntervalMs(milliseconds)
/** Schedule for execution after `dur`.
*
* Note: it most cases [[delay()]] is a better alternative.
*/
inline def setTimeout(inline dur: Duration): CallbackTo[Callback.SetTimeoutResult] =
toCallback.setTimeout(dur)
/** Schedule for execution after `dur`.
*
* Note: it most cases [[delay()]] is a better alternative.
*/
inline def setTimeout(inline dur: FiniteDuration): CallbackTo[Callback.SetTimeoutResult] =
toCallback.setTimeout(dur)
/** Schedule for execution after x milliseconds.
*
* Note: it most cases [[delayMs()]] is a better alternative.
*/
inline def setTimeoutMs(inline milliseconds: Double): CallbackTo[Callback.SetTimeoutResult] =
toCallback.setTimeoutMs(milliseconds)
/** Wraps this callback in a `try-finally` block and runs the given callback in the `finally` clause, after the
* current callback completes, be it in error or success.
*/
def finallyRun[B](runFinally: AsyncCallback[B]): AsyncCallback[A] =
attempt.flatMap {
case Right(a) => runFinally.ret(a)
case Left(e) => runFinally.attempt >> AsyncCallback.throwException(e)
}
/** Start both this and the given callback at once use the first result to become available,
* regardless of whether it's a success or failure.
*/
def race[B](that: AsyncCallback[B]): AsyncCallback[Either[A, B]] =
AsyncCallback.firstS(s => f =>
this.underlyingRepr(s)(e => f(e.map(Left(_)))) >>
that.underlyingRepr(s)(e => f(e.map(Right(_)))))
def toCallback: Callback =
AsyncCallback.newState.flatMap(underlyingRepr(_)(AsyncCallback.defaultCompleteWith))
def asCallbackToFuture: CallbackTo[Future[A]] =
AsyncCallback.newState.flatMap(s => CallbackTo {
val p = scala.concurrent.Promise[A]()
underlyingRepr(s)(t => Callback(p.tryComplete(t))).runNow()
p.future
})
def asCallbackToJsPromise: CallbackTo[js.Promise[A]] =
AsyncCallback.newState.flatMap(s =>
CallbackTo.newJsPromise[A].flatMap { case (p, pc) =>
underlyingRepr(s)(pc).map { _ =>
p
}
}
)
def unsafeToFuture(): Future[A] =
asCallbackToFuture.runNow()
def unsafeToJsPromise(): js.Promise[A] =
asCallbackToJsPromise.runNow()
/** Returns a synchronous [[Callback]] that when run, returns the result on the [[Right]] if this [[AsyncCallback]]
* is actually synchronous, else returns a new [[AsyncCallback]] on the [[Left]] that waits for the async computation
* to complete.
*/
def sync: CallbackTo[Either[AsyncCallback[A], A]] =
CallbackTo {
var result = Option.empty[A]
val promise = tap(a => result = Some(a)).asCallbackToJsPromise.runNow()
result match {
case Some(a) => Right(a)
case None => Left(AsyncCallback.fromJsPromise(promise))
}
}
def runNow(): Unit =
toCallback.runNow()
/** THIS IS VERY CONVENIENT IN UNIT TESTS BUT DO NOT RUN THIS IN PRODUCTION CODE.
*
* Executes this now, expecting and returning a synchronous result.
* If there are any asynchronous computations this will throw an exception.
*/
def unsafeRunNowSync(): A =
sync.runNow() match {
case Right(a) => a
case Left(_) => throw new RuntimeException(
"AsyncCallback#unsafeRunNowSync() failed! The AsyncCallback contains at least one asynchronous computation.")
}
def flatMapSync[B](f: A => CallbackTo[B]): AsyncCallback[B] =
flatMap(f(_).asAsyncCallback)
def flattenSync[B](using ev: A => CallbackTo[B]): AsyncCallback[B] =
flatten(using ev(_).asAsyncCallback)
def flatTapSync[B](t: A => CallbackTo[B]): AsyncCallback[A] =
flatTap(t(_).asAsyncCallback)
def handleErrorSync[AA >: A](f: Throwable => CallbackTo[AA]): AsyncCallback[AA] =
handleError(f(_).asAsyncCallback)
def maybeHandleErrorSync[AA >: A](f: PartialFunction[Throwable, CallbackTo[AA]]): AsyncCallback[AA] =
maybeHandleError(f.andThen(_.asAsyncCallback))
/** Wraps this callback in a `try-finally` block and runs the given callback in the `finally` clause, after the
* current callback completes, be it in error or success.
*/
inline def finallyRunSync[B](runFinally: CallbackTo[B]): AsyncCallback[A] =
finallyRun(runFinally.asAsyncCallback)
/** Runs this async computation in the background.
*
* Returns the ability for you to await/join the forked computation.
*/
def fork: CallbackTo[AsyncCallback.Forked[A]] =
AsyncCallback.promise[A].flatMap { case (promise, completePromise) =>
var _complete = false
val isComplete = CallbackTo(_complete)
val markComplete = CallbackTo { _complete = true }
val runInBg = self.attemptTry.finallyRunSync(markComplete).flatMapSync(completePromise).fork_
val forked = AsyncCallback.Forked(promise, isComplete)
runInBg.ret(forked)
}
/** Runs this async computation in the background.
*
* Unlike [[fork]] this returns nothing, meaning this is like fire-and-forget.
*/
inline def fork_ : Callback =
delayMs(1).toCallback
/** Runs this async computation in the background.
*
* Unlike [[fork_]] this returns an `AsyncCallback[Unit]` instead of a `Callback`.
*/
def dispatch: AsyncCallback[Unit] =
delayMs(1).void
/** Record the duration of this callback's execution. */
def withDuration[B](f: (A, FiniteDuration) => AsyncCallback[B]): AsyncCallback[B] = {
val nowMS: AsyncCallback[Long] = CallbackTo.currentTimeMillis.asAsyncCallback
for {
s <- nowMS
a <- self
e <- nowMS
b <- f(a, FiniteDuration(e - s, MILLISECONDS))
} yield b
}
/** Log the duration of this callback's execution. */
def logDuration(fmt: FiniteDuration => String): AsyncCallback[A] =
withDuration((a, d) =>
Callback.log(fmt(d)).asAsyncCallback ret a)
/** Log the duration of this callback's execution.
*
* @param name Prefix to appear the log output.
*/
def logDuration(name: String): AsyncCallback[A] =
logDuration(d => s"$name completed in $d.")
/** Log the duration of this callback's execution. */
def logDuration: AsyncCallback[A] =
logDuration("AsyncCallback")
def withFilter(f: A => Boolean): AsyncCallback[A] =
map[A](a => if f(a) then a else
// This is what scala.Future does
throw new NoSuchElementException("AsyncCallback.withFilter predicate is not satisfied"))
def onCancel(f: AsyncCallback[Unit]): AsyncCallback[A] =
new AsyncCallback(s => {
val i = s.onCancel(f)
self
.map { a => s.unCancel(i); a }
.underlyingRepr(s)
})
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy