Created
April 19, 2017 14:22
-
-
Save torstenrudolf/edb952fc1d028b6d208513caff0eee74 to your computer and use it in GitHub Desktop.
FetchingPotMap
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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