Skip to content

Instantly share code, notes, and snippets.

@torstenrudolf
Created April 19, 2017 14:22
Show Gist options
  • Save torstenrudolf/edb952fc1d028b6d208513caff0eee74 to your computer and use it in GitHub Desktop.
Save torstenrudolf/edb952fc1d028b6d208513caff0eee74 to your computer and use it in GitHub Desktop.
FetchingPotMap
package frontend.utils.Diode
import diode.Implicits.runAfterImpl
import diode._
import diode.data._
import sharedCode.dataTransportObjects.UTCDateTime
import sharedCode.databaseQueryUtils.{DBColumnIdentifier, DBColumnSortingSpec}
import scala.collection.GenTraversableOnce
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import scala.util.{Failure, Success}
/**
* encapsulated elems Map in order to prevent multiple times fetching of same elements
* that are accessed from different components at same time
*
* we keep a mutable `pendingElems`, to prevent refetching before the AppModel has been updated
*
* Currently everywhere that is using FetchingPotMaps they have to use MapWithPendingCache for the elems.
* It would be nice to use MapWithPendingCache only internally and not show to the outside world at all.
* But since `copy()` needs to be implemented at the childClasses, I think this might only be avoidable with the help of macros.
*/
case class MapWithPendingCache[K, V](elems: Map[K, PotVal[V]]) {
private[Diode] val pendingElems: scala.collection.mutable.Map[K, PotVal[V]] =
scala.collection.mutable.Map.empty[K, PotVal[V]]
private[Diode] def addPendingElems(keys: GenTraversableOnce[K]): Unit =
pendingElems ++= keys.toList.map(k => (k, PotVal.Pending[V]))
def copy(newElems: Map[K, PotVal[V]] = elems): MapWithPendingCache[K, V] = {
val newMap = new MapWithPendingCache(elems = newElems)
newMap.pendingElems ++= this.pendingElems
newMap
}
def get(k: K): Option[PotVal[V]] = pendingElems.get(k).map(Some(_)).getOrElse(elems.get(k))
def +(kv: (K, PotVal[V])): MapWithPendingCache[K, V] = {
pendingElems -= kv._1
this.copy(newElems = elems + kv)
}
def ++(xs: GenTraversableOnce[(K, PotVal[V])]): MapWithPendingCache[K, V] = {
val xsX = xs.toList
pendingElems --= xsX.map(_._1)
this.copy(newElems = elems ++ xsX)
}
def -(kv: K): MapWithPendingCache[K, V] = {
pendingElems -= kv
this.copy(newElems = elems - kv)
}
def --(xs: GenTraversableOnce[K]): MapWithPendingCache[K, V] = {
val xsX = xs.toList
pendingElems --= xsX
this.copy(newElems = elems -- xsX)
}
def filter(f: ((K, PotVal[V])) => Boolean) = this.copy(newElems = elems.filter(f))
def mapValues[X](f: PotVal[V] => PotVal[X]): MapWithPendingCache[K, X] = new MapWithPendingCache(elems.mapValues(f))
}
object MapWithPendingCache {
def empty[K, V] = new MapWithPendingCache[K, V](elems = Map.empty[K, PotVal[V]])
}
case class FetchingPotMap[K, V](
//@param asyncCall: must return a map of exactly the keys that were fed in!
private val fetchFunc: Set[K] => Future[Map[K, V]],
private[Diode] val elems: MapWithPendingCache[K, V] = MapWithPendingCache.empty[K, V],
private val partialUpdateAction: Traversable[(K, PotVal[V])] => Action,
private val dispatcher: Dispatcher,
private val expiryTimeMS: Option[Long] = None
)
extends BaseFetchingPotMap[K, V, FetchingPotMap[K, V]](
elems = elems,
partialUpdateAction = partialUpdateAction,
dispatcher = dispatcher,
expiryTimeMS = expiryTimeMS
)
{
override protected def copy(newElems: MapWithPendingCache[K, V]): FetchingPotMap[K, V] =
FetchingPotMap(this.fetchFunc, newElems, this.partialUpdateAction, this.dispatcher, this.expiryTimeMS)
override protected def asyncCall(keys: Set[K]): Future[Map[K, V]] = this.fetchFunc(keys)
}
/**
* An extension of diode.data.PotMap
*
* This is a wrapper around a map.
* When accessing elements and those elements are not already fetched, it triggers a fetch of them automatically
* and the resultValue is wrapped inside a diode.data.Pot ("potential value").
*
* You need to supply the partialUpdateAction, in order to update the FetchingPotMap instance inside the AppStateModel.
*
* @param elems a map holding the fetched (or pending) elements
* @param partialUpdateAction this action should only change the given keys in the model
* @param dispatcher the AppCircuit action dispatch function
* @param expiryTimeMS expiry time of the fetched objects
*/
abstract class BaseFetchingPotMap[K, V, SELF <: BaseFetchingPotMap[K, V, SELF]]
(elems: MapWithPendingCache[K, V] = MapWithPendingCache.empty[K, V],
partialUpdateAction: Traversable[(K, PotVal[V])] => Action,
dispatcher: Dispatcher,
expiryTimeMS: Option[Long] = None
) {
fetchingPotMap =>
protected def asyncCall(keys: Set[K]): Future[Map[K, V]]
protected def copy(newElems: MapWithPendingCache[K, V]): SELF
def updated(key: K, value: PotVal[V]) = copy(elems + (key -> value))
def updated(kvs: Traversable[(K, PotVal[V])]) = copy(elems ++ kvs)
def remove(key: K) = copy(elems - key)
def remove(keys: Traversable[K]) = copy(elems -- keys)
def removeWhere(f: (K, PotVal[V]) => Boolean) = copy(elems.filter { case (k, v) => f(k, v) })
def clear = copy(MapWithPendingCache.empty[K, V])
def refresh(key: K, keepStaleData: Boolean): Unit = refresh(Traversable(key), keepStaleData)
def refresh(keys: Traversable[K], keepStaleData: Boolean): Unit = {
// set current vals to pending
// trigger asyncCall and let it dispatcher the update action after it has run
if (keys.isEmpty) return
val keySet = keys.toSet
// see MapWithPendingCache -- a cache to prevent multi fetching of same elems before model update
elems.addPendingElems(keySet)
// set elems in model to pending (this triggers re-rendering of pages and is still needed,
// even though we keep "internal cache" of pending elems)
runAfterImpl.runAfter(0) {
dispatcher(partialUpdateAction(
keySet.map(k =>
elems.get(k) match {
case Some(e) => (k, if (keepStaleData) e.pending() else PotVal.Pending[V])
case None => (k, PotVal.Pending[V])
}
)
.toMap
))
}
runAfterImpl.runAfter(0) {
asyncCall(keySet).onComplete {
case Success(result) =>
assert(result.keySet == keySet, "`FetchingPotMap.asyncCall` must return a map of exactly the keys that were fed in!")
dispatcher(partialUpdateAction(
result.mapValues(v => PotVal(value = diode.data.Ready(v), fetchedAt = Some(UTCDateTime.currentTime)))
))
case Failure(t) => dispatcher(partialUpdateAction(
keySet.map(k => elems.get(k) match {
case Some(v) => (k, v.fail(t))
case _ => (k, PotVal.Failed[V](t))
})
))
}
}
}
/**
* @return (needRefresh, element)
*/
private def getAndCheckIfRefreshNeeded(key: K): (Boolean, Pot[V]) = {
elems.get(key) match {
case Some(elem) if elem.state == PotState.PotEmpty || (elem.value.isReady && (expiryTimeMS zip elem.fetchedAt).exists { case (et, fa) => et < (UTCDateTime.currentTime - fa) }) =>
(true, elem.value.pending())
case None =>
(true, Pending().asInstanceOf[Pot[V]])
case Some(elem) =>
(false, elem.value)
}
}
def get(key: K): Pot[V] = {
getAndCheckIfRefreshNeeded(key) match {
case (true, result) =>
refresh(key, keepStaleData = true)
result
case (false, result) =>
result
}
}
def hasFetched(key: K): Boolean = {
!this.getAndCheckIfRefreshNeeded(key)._1
}
def get(keys: Traversable[K]): Traversable[(K, Pot[V])] = {
val (keysToRefresh, returnValues) = keys.map(key =>
getAndCheckIfRefreshNeeded(key) match {
case (true, result) =>
(Some(key), (key, result))
case (false, result) =>
(None, (key, result))
}
).unzip
keysToRefresh.collect { case Some(k) => k }.toList match {
case Nil => Unit
case someKeys => refresh(someKeys.toSet, keepStaleData = true)
}
returnValues
}
def getPaged(keys: Traversable[K], pageSize: Int, currentPage: Int): Pot[PagedData[V]] = {
require(currentPage > 0, s"currentPage not positive ($currentPage)")
require(pageSize > 0, s"pageSize not positive ($pageSize)")
val startIndex = pageSize * (currentPage - 1)
val endIndex = startIndex + pageSize
this.get(keys.slice(startIndex, endIndex)).map(_._2).toList.transformedPotList
.map(fetchedData =>
PagedData[V](
fetchedData = fetchedData,
numTotalItems = keys.size,
pageSize = pageSize,
currentPage = currentPage
)
)
}
def dataGetter(keys: Traversable[K]): DataGetter[V] = {
new DataGetter[V] {
override def pagedQuery(pageSize: Int, currentPage: Int): Pot[PagedData[V]] = {
fetchingPotMap.getPaged(keys, pageSize, currentPage)
}
override def queryAll(): Pot[List[V]] = fetchingPotMap.get(keys).map(_._2).toList.transformedPotList
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment