Skip to content

Commit 06dd108

Browse files
committed
Add AsyncCallback.ref
Closes #817
1 parent c96f66d commit 06dd108

File tree

3 files changed

+120
-0
lines changed

3 files changed

+120
-0
lines changed

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

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,92 @@ object AsyncCallback {
447447
/** Creates a new (non-reentrant) read/write mutex. */
448448
def readWriteMutex: CallbackTo[ReadWriteMutex] =
449449
CallbackTo(new ReadWriteMutex)
450+
451+
// ===================================================================================================================
452+
453+
final class Ref[A] private[AsyncCallback](atomicReads: Boolean, atomicWrites: Boolean) {
454+
455+
private val mutex = readWriteMutex.runNow()
456+
private val initialised = barrier.runNow()
457+
private var _value: A = _
458+
459+
private val markInitialised =
460+
initialised.complete.asAsyncCallback
461+
462+
/** If this hasn't been set yet, it will block until it is set. */
463+
val get: AsyncCallback[A] = {
464+
var readValue: AsyncCallback[A] =
465+
AsyncCallback.delay(_value)
466+
467+
if (atomicReads)
468+
readValue = mutex.read(readValue)
469+
470+
initialised.await >> readValue
471+
}
472+
473+
/** Synchronously return whatever value is currently stored. (Ignores atomicity). */
474+
lazy val getIfAvailable: CallbackTo[Option[A]] =
475+
initialised.isComplete.map {
476+
case true => Some(_value)
477+
case false => None
478+
}
479+
480+
private def inWriteMutex[B](a: AsyncCallback[B]): AsyncCallback[B] =
481+
if (atomicWrites)
482+
mutex.write(a)
483+
else
484+
a
485+
486+
// This must only be called within the write mutex
487+
private def setWithinMutex(c: AsyncCallback[A]): AsyncCallback[Unit] =
488+
for {
489+
a <- c
490+
_ <- AsyncCallback.delay { _value = a }
491+
_ <- markInitialised
492+
} yield ()
493+
494+
def set(a: => A): AsyncCallback[Unit] =
495+
setAsync(AsyncCallback.delay(a))
496+
497+
def setSync(c: CallbackTo[A]): AsyncCallback[Unit] =
498+
setAsync(c.asAsyncCallback)
499+
500+
def setAsync(c: AsyncCallback[A]): AsyncCallback[Unit] =
501+
inWriteMutex(setWithinMutex(c))
502+
503+
/** Returns whether or not the value was set. */
504+
def setIfUnset(a: => A): AsyncCallback[Boolean] =
505+
setIfUnsetAsync(AsyncCallback.delay(a))
506+
507+
/** Returns whether or not the value was set. */
508+
def setIfUnsetSync(c: CallbackTo[A]): AsyncCallback[Boolean] =
509+
setIfUnsetAsync(c.asAsyncCallback)
510+
511+
/** Returns whether or not the value was set. */
512+
def setIfUnsetAsync(c: AsyncCallback[A]): AsyncCallback[Boolean] =
513+
initialised.isComplete.asAsyncCallback.flatMap {
514+
case true => AsyncCallback.pure(false)
515+
case false =>
516+
inWriteMutex {
517+
AsyncCallback.byName {
518+
if (initialised.isComplete.runNow())
519+
AsyncCallback.pure(false)
520+
else
521+
setWithinMutex(c).ret(true)
522+
}
523+
}
524+
}
525+
}
526+
527+
@inline def ref[A]: CallbackTo[Ref[A]] =
528+
ref()
529+
530+
def ref[A](allowStaleReads: Boolean = false,
531+
atomicWrites : Boolean = true): CallbackTo[Ref[A]] =
532+
CallbackTo(new Ref(
533+
atomicReads = atomicWrites && !allowStaleReads,
534+
atomicWrites = atomicWrites,
535+
))
450536
}
451537

452538
// █████████████████████████████████████████████████████████████████████████████████████████████████████████████████████

doc/changelog/1.7.7.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,24 @@ This entire release is focused on `AsyncCallback`.
6969
}
7070
```
7171

72+
* Added `ref[A](allowStaleReads = false, atomicWrites = true)` which returns a `AsyncCallback.Ref`.
73+
A ref is effectively a "wrapper" for mutable variable. By default, setters and getters are atomic.
74+
75+
```scala
76+
AsyncCallback.Ref[A] {
77+
val get : AsyncCallback[A]
78+
val getIfAvailable: CallbackTo[Option[A]]
79+
80+
def set (a: => A) : AsyncCallback[Unit]
81+
def setSync (c: CallbackTo[A]) : AsyncCallback[Unit]
82+
def setAsync(c: AsyncCallback[A]): AsyncCallback[Unit]
83+
84+
def setIfUnset (a: => A) : AsyncCallback[Boolean]
85+
def setIfUnsetSync (c: CallbackTo[A]) : AsyncCallback[Boolean]
86+
def setIfUnsetAsync(c: AsyncCallback[A]): AsyncCallback[Boolean]
87+
}
88+
```
89+
7290
* You can now add a `_` suffix to the following to return an `AsyncCallback[Unit]` and be more efficient under-the-hood:
7391

7492
* `traverse`

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,5 +281,21 @@ object AsyncCallbackTest extends TestSuite {
281281
} yield ()
282282
}
283283

284+
"ref" - asyncTest {
285+
for {
286+
ref <- AsyncCallback.ref[Int].asAsyncCallback
287+
g1 <- ref.get.fork.asAsyncCallback
288+
_ <- ref.getIfAvailable.asAsyncCallback.tap(assertEq(_, None))
289+
_ <- ref.setIfUnset(123)
290+
_ <- ref.getIfAvailable.asAsyncCallback.tap(assertEq(_, Some(123)))
291+
_ <- ref.setIfUnset(456)
292+
_ <- ref.getIfAvailable.asAsyncCallback.tap(assertEq(_, Some(123)))
293+
_ <- g1.await.tap(assertEq(_, 123))
294+
_ <- ref.set(987)
295+
_ <- ref.getIfAvailable.asAsyncCallback.tap(assertEq(_, Some(987)))
296+
_ <- ref.get.tap(assertEq(_, 987))
297+
} yield ()
298+
}
299+
284300
}
285301
}

0 commit comments

Comments
 (0)