Skip to content

Commit 0fe692c

Browse files
committed
Add ability to fork AsyncCallbacks
Closes #819
1 parent 7082fba commit 0fe692c

File tree

3 files changed

+54
-1
lines changed

3 files changed

+54
-1
lines changed

core/src/main/scala/japgolly/scalajs/react/AsyncCallback.scala

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,8 @@ object AsyncCallback {
220220
promise
221221
}
222222
}
223+
224+
final case class Forked[A](await: AsyncCallback[A], isComplete: CallbackTo[Boolean])
223225
}
224226

225227
/** Pure asynchronous callback.
@@ -252,7 +254,7 @@ object AsyncCallback {
252254
*
253255
* @tparam A The type of data asynchronously produced on success.
254256
*/
255-
final class AsyncCallback[A] private[AsyncCallback] (val completeWith: (Try[A] => Callback) => Callback) extends AnyVal {
257+
final class AsyncCallback[A] private[AsyncCallback] (val completeWith: (Try[A] => Callback) => Callback) extends AnyVal { self =>
256258

257259
@inline def underlyingRepr = completeWith
258260

@@ -704,4 +706,26 @@ final class AsyncCallback[A] private[AsyncCallback] (val completeWith: (Try[A] =
704706
*/
705707
@inline def finallyRunSync[B](runFinally: CallbackTo[B]): AsyncCallback[A] =
706708
finallyRun(runFinally.asAsyncCallback)
709+
710+
/** Runs this async computation in the background.
711+
*
712+
* Returns the ability for you to await/join the forked computation.
713+
*/
714+
def fork: CallbackTo[AsyncCallback.Forked[A]] =
715+
AsyncCallback.promise[A].flatMap { case (promise, completePromise) =>
716+
var _complete = false
717+
val isComplete = CallbackTo(_complete)
718+
val markComplete = CallbackTo { _complete = true }
719+
val runInBg = self.attemptTry.finallyRunSync(markComplete).flatMapSync(completePromise).fork_
720+
val forked = AsyncCallback.Forked(promise, isComplete)
721+
runInBg.ret(forked)
722+
}
723+
724+
/** Runs this async computation in the background.
725+
*
726+
* Unlike [[fork]] this returns nothing, meaning this is like fire-and-forget.
727+
*/
728+
def fork_ : Callback =
729+
delayMs(1).toCallback
730+
707731
}

doc/changelog/1.7.7.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22

33
This entire release is focused on `AsyncCallback`.
44

5+
* `AsyncCallback` instances
6+
7+
* Added `fork` and `fork_` which run the async computation in the background.
8+
9+
The result of `fork` gives you access to `await: AsyncCallback[A], isComplete: CallbackTo[Boolean]` which you can
10+
use to join back up with it again later.
11+
12+
`fork_` on the other hand, returns nothing and is effectively fire-and-forget.
13+
514
* `AsyncCallback.Barrier`:
615

716
* Added `isComplete: CallbackTo[Boolean]` to synchronously query whether the barrier is complete or not.

test/src/test/scala/japgolly/scalajs/react/core/AsyncCallbackTest.scala

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,15 @@ object AsyncCallbackTest extends TestSuite {
1717
def apply(s: String) = Callback { this += s }
1818
}
1919

20+
private final val asyncTestTimeout = 2000
21+
22+
private def asyncTest[A](ac: AsyncCallback[A]): Future[A] = {
23+
ac.timeoutMs(asyncTestTimeout).map {
24+
case Some(a) => a
25+
case None => fail(s"Async test timed out after ${asyncTestTimeout / 1000} sec.")
26+
}.unsafeToFuture()
27+
}
28+
2029
override def tests = Tests {
2130

2231
"fromCallback" - {
@@ -144,5 +153,16 @@ object AsyncCallbackTest extends TestSuite {
144153
assertEq(i, 2)
145154
}
146155

156+
"fork" - asyncTest {
157+
for {
158+
(task, completeTask) <- AsyncCallback.promise[Int].asAsyncCallback
159+
forked <- task.fork.asAsyncCallback
160+
_ <- forked.isComplete.asAsyncCallback.tap(assertEq(_, false))
161+
_ <- completeTask(Success(123)).asAsyncCallback
162+
_ <- forked.await.tap(assertEq(_, 123))
163+
_ <- forked.isComplete.asAsyncCallback.tap(assertEq(_, true))
164+
} yield ()
165+
}
166+
147167
}
148168
}

0 commit comments

Comments
 (0)