mirror of
				https://github.com/InsanusMokrassar/MicroUtils.git
				synced 2025-11-04 06:00:22 +00:00 
			
		
		
		
	@@ -1,5 +1,13 @@
 | 
			
		||||
# Changelog
 | 
			
		||||
 | 
			
		||||
## 0.25.1
 | 
			
		||||
 | 
			
		||||
* `Coroutines`:
 | 
			
		||||
  * Add `SortedMapLikeBinaryTreeNode`
 | 
			
		||||
* `Pagination`:
 | 
			
		||||
  * `Compose`:
 | 
			
		||||
    * One more rework of `InfinityPagedComponent` and `PagedComponent`
 | 
			
		||||
 | 
			
		||||
## 0.25.0
 | 
			
		||||
 | 
			
		||||
* `Repos`:
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
package dev.inmo.micro_utils.coroutines.collections
 | 
			
		||||
 | 
			
		||||
import dev.inmo.micro_utils.coroutines.SmartRWLocker
 | 
			
		||||
import dev.inmo.micro_utils.coroutines.waitReadRelease
 | 
			
		||||
import dev.inmo.micro_utils.coroutines.withReadAcquire
 | 
			
		||||
import dev.inmo.micro_utils.coroutines.withWriteLock
 | 
			
		||||
import kotlinx.coroutines.job
 | 
			
		||||
@@ -93,7 +92,7 @@ class SortedBinaryTreeNode<T>(
 | 
			
		||||
 * This process will continue until function will not find place to put [SortedBinaryTreeNode] with data or
 | 
			
		||||
 * [SortedBinaryTreeNode] with [SortedBinaryTreeNode.data] same as [newData] will be found
 | 
			
		||||
 */
 | 
			
		||||
private suspend fun <T> SortedBinaryTreeNode<T>.addSubNode(
 | 
			
		||||
private suspend fun <T> SortedBinaryTreeNode<T>.upsertSubNode(
 | 
			
		||||
    subNode: SortedBinaryTreeNode<T>,
 | 
			
		||||
    skipLockers: Set<SmartRWLocker> = emptySet()
 | 
			
		||||
): SortedBinaryTreeNode<T> {
 | 
			
		||||
@@ -149,7 +148,7 @@ private suspend fun <T> SortedBinaryTreeNode<T>.addSubNode(
 | 
			
		||||
 * [SortedBinaryTreeNode] with [SortedBinaryTreeNode.data] same as [newData] will be found
 | 
			
		||||
 */
 | 
			
		||||
suspend fun <T> SortedBinaryTreeNode<T>.addSubNode(newData: T): SortedBinaryTreeNode<T> {
 | 
			
		||||
    return addSubNode(
 | 
			
		||||
    return upsertSubNode(
 | 
			
		||||
        SortedBinaryTreeNode(newData, comparator)
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
@@ -198,8 +197,8 @@ suspend fun <T> SortedBinaryTreeNode<T>.findParentNode(data: T): SortedBinaryTre
 | 
			
		||||
 */
 | 
			
		||||
suspend fun <T> SortedBinaryTreeNode<T>.removeSubNode(data: T): Pair<SortedBinaryTreeNode<T>, SortedBinaryTreeNode<T>>? {
 | 
			
		||||
    val onFoundToRemoveCallback: suspend SortedBinaryTreeNode<T>.(left: SortedBinaryTreeNode<T>?, right: SortedBinaryTreeNode<T>?) -> Unit = { left, right ->
 | 
			
		||||
        left ?.also { leftNode -> addSubNode(leftNode, setOf(locker)) }
 | 
			
		||||
        right ?.also { rightNode -> addSubNode(rightNode, setOf(locker)) }
 | 
			
		||||
        left ?.also { leftNode -> upsertSubNode(leftNode, setOf(locker)) }
 | 
			
		||||
        right ?.also { rightNode -> upsertSubNode(rightNode, setOf(locker)) }
 | 
			
		||||
    }
 | 
			
		||||
    while (coroutineContext.job.isActive) {
 | 
			
		||||
        val foundParentNode = findParentNode(data) ?: return null
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,401 @@
 | 
			
		||||
package dev.inmo.micro_utils.coroutines.collections
 | 
			
		||||
 | 
			
		||||
import dev.inmo.micro_utils.coroutines.SmartRWLocker
 | 
			
		||||
import dev.inmo.micro_utils.coroutines.withReadAcquire
 | 
			
		||||
import dev.inmo.micro_utils.coroutines.withWriteLock
 | 
			
		||||
import kotlinx.coroutines.job
 | 
			
		||||
import kotlinx.serialization.Serializable
 | 
			
		||||
import kotlin.coroutines.coroutineContext
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Creates simple [Comparator] which will use [compareTo] of [T] for both objects
 | 
			
		||||
 */
 | 
			
		||||
private fun <T : Comparable<C>, C : T> T.createComparator() = Comparator<C> { o1, o2 -> o1.compareTo(o2) }
 | 
			
		||||
 | 
			
		||||
@Serializable
 | 
			
		||||
class SortedMapLikeBinaryTreeNode<K, V>(
 | 
			
		||||
    val key: K,
 | 
			
		||||
    val value: V,
 | 
			
		||||
    internal val comparator: Comparator<K>,
 | 
			
		||||
) : Iterable<SortedMapLikeBinaryTreeNode<K, V>> {
 | 
			
		||||
    internal var leftNode: SortedMapLikeBinaryTreeNode<K, V>? = null
 | 
			
		||||
    internal var rightNode: SortedMapLikeBinaryTreeNode<K, V>? = null
 | 
			
		||||
    internal val locker: SmartRWLocker by lazy {
 | 
			
		||||
        SmartRWLocker()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend fun getLeftNode() = locker.withReadAcquire {
 | 
			
		||||
        leftNode
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend fun getRightNode() = locker.withReadAcquire {
 | 
			
		||||
        rightNode
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend fun getLeftKey() = getLeftNode() ?.key
 | 
			
		||||
    suspend fun getLeftValue() = getLeftNode() ?.value
 | 
			
		||||
 | 
			
		||||
    suspend fun getRightKey() = getRightNode() ?.value
 | 
			
		||||
    suspend fun getRightValue() = getRightNode() ?.value
 | 
			
		||||
 | 
			
		||||
    override fun equals(other: Any?): Boolean {
 | 
			
		||||
        return other === this || (other is SortedMapLikeBinaryTreeNode<*, *> && other.key == key && other.rightNode == rightNode && other.leftNode == leftNode)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun hashCode(): Int {
 | 
			
		||||
        return key.hashCode() * 31 + rightNode.hashCode() + leftNode.hashCode()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    suspend fun size(): Int {
 | 
			
		||||
        return locker.withReadAcquire {
 | 
			
		||||
            1 + (leftNode ?.size() ?: 0) + (rightNode ?.size() ?: 0)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * This [Iterator] will run from less to greater values of nodes starting the
 | 
			
		||||
     * [dev.inmo.micro_utils.coroutines.collections.SortedMapLikeBinaryTreeNode]-receiver. Due to non-suspending
 | 
			
		||||
     * nature of [iterator] builder, this [Iterator] **DO NOT** guarantee consistent content due to iterations. It
 | 
			
		||||
     * means, that tree can be changed during to iteration process
 | 
			
		||||
     */
 | 
			
		||||
    override fun iterator(): Iterator<SortedMapLikeBinaryTreeNode<K, V>> = iterator {
 | 
			
		||||
        leftNode ?.let {
 | 
			
		||||
            it.iterator().forEach { yield(it) }
 | 
			
		||||
        }
 | 
			
		||||
        yield(this@SortedMapLikeBinaryTreeNode)
 | 
			
		||||
        rightNode ?.let {
 | 
			
		||||
            it.iterator().forEach { yield(it) }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun toString(): String {
 | 
			
		||||
        return "$key($leftNode;$rightNode)"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        operator fun <K : Comparable<K>, V> invoke(
 | 
			
		||||
            key: K,
 | 
			
		||||
            value: V
 | 
			
		||||
        ) = SortedMapLikeBinaryTreeNode(
 | 
			
		||||
            key,
 | 
			
		||||
            value,
 | 
			
		||||
            key.createComparator()
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Will add subnode in tree if there are no any node with [newData]
 | 
			
		||||
 *
 | 
			
		||||
 * * If [newData] is greater than [SortedMapLikeBinaryTreeNode.key] of currently checking node,
 | 
			
		||||
 * will be used [SortedMapLikeBinaryTreeNode.rightNode]
 | 
			
		||||
 * * If [newData] is equal to [SortedMapLikeBinaryTreeNode.key] of currently
 | 
			
		||||
 * checking node - will be returned currently checking node
 | 
			
		||||
 * * If [newData] is less than [SortedMapLikeBinaryTreeNode.key] of currently
 | 
			
		||||
 * checking node - will be used [SortedMapLikeBinaryTreeNode.leftNode]
 | 
			
		||||
 *
 | 
			
		||||
 * This process will continue until function will not find place to put [SortedMapLikeBinaryTreeNode] with data or
 | 
			
		||||
 * [SortedMapLikeBinaryTreeNode] with [SortedMapLikeBinaryTreeNode.key] same as [newData] will be found
 | 
			
		||||
 *
 | 
			
		||||
 * @param replaceMode Will replace only value if node already exists
 | 
			
		||||
 */
 | 
			
		||||
private suspend fun <K, V> SortedMapLikeBinaryTreeNode<K, V>.upsertSubNode(
 | 
			
		||||
    subNode: SortedMapLikeBinaryTreeNode<K, V>,
 | 
			
		||||
    skipLockers: Set<SmartRWLocker> = emptySet(),
 | 
			
		||||
    replaceMode: Boolean
 | 
			
		||||
): SortedMapLikeBinaryTreeNode<K, V> {
 | 
			
		||||
    var currentlyChecking = this
 | 
			
		||||
    var latestParent: SortedMapLikeBinaryTreeNode<K, V>? = null
 | 
			
		||||
    val lockedLockers = mutableSetOf<SmartRWLocker>()
 | 
			
		||||
    try {
 | 
			
		||||
        while (coroutineContext.job.isActive) {
 | 
			
		||||
            if (currentlyChecking.locker !in lockedLockers && currentlyChecking.locker !in skipLockers) {
 | 
			
		||||
                currentlyChecking.locker.lockWrite()
 | 
			
		||||
                lockedLockers.add(currentlyChecking.locker)
 | 
			
		||||
            }
 | 
			
		||||
            val left = currentlyChecking.leftNode
 | 
			
		||||
            val right = currentlyChecking.rightNode
 | 
			
		||||
            val comparingResult = currentlyChecking.comparator.compare(subNode.key, currentlyChecking.key)
 | 
			
		||||
            val isGreater = comparingResult > 0
 | 
			
		||||
            when {
 | 
			
		||||
                comparingResult == 0 -> {
 | 
			
		||||
                    val resultNode = if (replaceMode) {
 | 
			
		||||
                        subNode
 | 
			
		||||
                    } else {
 | 
			
		||||
                        val newNode = SortedMapLikeBinaryTreeNode(
 | 
			
		||||
                            subNode.key,
 | 
			
		||||
                            subNode.value,
 | 
			
		||||
                            currentlyChecking.comparator,
 | 
			
		||||
                        )
 | 
			
		||||
                        newNode.leftNode = currentlyChecking.leftNode
 | 
			
		||||
                        newNode.rightNode = currentlyChecking.rightNode
 | 
			
		||||
                        newNode
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    latestParent ?.let {
 | 
			
		||||
                        when {
 | 
			
		||||
                            it.leftNode === currentlyChecking -> it.leftNode = resultNode
 | 
			
		||||
                            it.rightNode === currentlyChecking -> it.rightNode = resultNode
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return resultNode
 | 
			
		||||
                }
 | 
			
		||||
                isGreater && right == null -> {
 | 
			
		||||
                    currentlyChecking.rightNode = subNode
 | 
			
		||||
                    return subNode
 | 
			
		||||
                }
 | 
			
		||||
                isGreater && right != null -> {
 | 
			
		||||
                    latestParent = currentlyChecking
 | 
			
		||||
                    currentlyChecking = right
 | 
			
		||||
                }
 | 
			
		||||
                left == null -> {
 | 
			
		||||
                    currentlyChecking.leftNode = subNode
 | 
			
		||||
                    return subNode
 | 
			
		||||
                }
 | 
			
		||||
                else -> {
 | 
			
		||||
                    latestParent = currentlyChecking
 | 
			
		||||
                    currentlyChecking = left
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    } finally {
 | 
			
		||||
        lockedLockers.forEach {
 | 
			
		||||
            runCatching { it.unlockWrite() }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    error("Unable to add node")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Will add subnode in tree if there are no any node with [key]
 | 
			
		||||
 *
 | 
			
		||||
 * * If [key] is greater than [SortedMapLikeBinaryTreeNode.key] of currently checking node,
 | 
			
		||||
 * will be used [SortedMapLikeBinaryTreeNode.rightNode]
 | 
			
		||||
 * * If [key] is equal to [SortedMapLikeBinaryTreeNode.key] of currently
 | 
			
		||||
 * checking node - will be returned currently checking node
 | 
			
		||||
 * * If [key] is less than [SortedMapLikeBinaryTreeNode.key] of currently
 | 
			
		||||
 * checking node - will be used [SortedMapLikeBinaryTreeNode.leftNode]
 | 
			
		||||
 *
 | 
			
		||||
 * This process will continue until function will not find place to put [SortedMapLikeBinaryTreeNode] with data or
 | 
			
		||||
 * [SortedMapLikeBinaryTreeNode] with [SortedMapLikeBinaryTreeNode.key] same as [key] will be found
 | 
			
		||||
 */
 | 
			
		||||
suspend fun <K, V> SortedMapLikeBinaryTreeNode<K, V>.upsertSubNode(
 | 
			
		||||
    key: K,
 | 
			
		||||
    value: V
 | 
			
		||||
): SortedMapLikeBinaryTreeNode<K, V> {
 | 
			
		||||
    return upsertSubNode(
 | 
			
		||||
        SortedMapLikeBinaryTreeNode(key, value, comparator),
 | 
			
		||||
        replaceMode = false
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
suspend fun <K, V> SortedMapLikeBinaryTreeNode<K, V>.findParentNode(data: K): SortedMapLikeBinaryTreeNode<K, V>? {
 | 
			
		||||
    var currentParent: SortedMapLikeBinaryTreeNode<K, V>? = null
 | 
			
		||||
    var currentlyChecking: SortedMapLikeBinaryTreeNode<K, V>? = this
 | 
			
		||||
    val lockedLockers = mutableSetOf<SmartRWLocker>()
 | 
			
		||||
    try {
 | 
			
		||||
        while (coroutineContext.job.isActive) {
 | 
			
		||||
            if (currentlyChecking == null) {
 | 
			
		||||
                return null
 | 
			
		||||
            }
 | 
			
		||||
            if (currentlyChecking.locker !in lockedLockers) {
 | 
			
		||||
                currentlyChecking.locker.acquireRead()
 | 
			
		||||
                lockedLockers.add(currentlyChecking.locker)
 | 
			
		||||
            }
 | 
			
		||||
            val comparingResult = currentlyChecking.comparator.compare(data, currentlyChecking.key)
 | 
			
		||||
            when {
 | 
			
		||||
                comparingResult > 0 -> {
 | 
			
		||||
                    currentParent = currentlyChecking
 | 
			
		||||
                    currentlyChecking = currentlyChecking.rightNode
 | 
			
		||||
                    continue
 | 
			
		||||
                }
 | 
			
		||||
                comparingResult < 0 -> {
 | 
			
		||||
                    currentParent = currentlyChecking
 | 
			
		||||
                    currentlyChecking = currentlyChecking.leftNode
 | 
			
		||||
                    continue
 | 
			
		||||
                }
 | 
			
		||||
                else -> return currentParent
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    } finally {
 | 
			
		||||
        lockedLockers.forEach {
 | 
			
		||||
            runCatching { it.releaseRead() }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    error("Unable to find node")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Will remove (detach) node from tree starting with [this] [SortedMapLikeBinaryTreeNode]
 | 
			
		||||
 *
 | 
			
		||||
 * @return If data were found, [Pair] where [Pair.first] is the parent node where from [Pair.second] has been detached;
 | 
			
		||||
 *         null otherwise
 | 
			
		||||
 */
 | 
			
		||||
suspend fun <K, V> SortedMapLikeBinaryTreeNode<K, V>.removeSubNode(data: K): Pair<SortedMapLikeBinaryTreeNode<K, V>, SortedMapLikeBinaryTreeNode<K, V>>? {
 | 
			
		||||
    val onFoundToRemoveCallback: suspend SortedMapLikeBinaryTreeNode<K, V>.(left: SortedMapLikeBinaryTreeNode<K, V>?, right: SortedMapLikeBinaryTreeNode<K, V>?) -> Unit = { left, right ->
 | 
			
		||||
        left ?.also { leftNode -> upsertSubNode(leftNode, setOf(locker), replaceMode = true) }
 | 
			
		||||
        right ?.also { rightNode -> upsertSubNode(rightNode, setOf(locker), replaceMode = true) }
 | 
			
		||||
    }
 | 
			
		||||
    while (coroutineContext.job.isActive) {
 | 
			
		||||
        val foundParentNode = findParentNode(data) ?: return null
 | 
			
		||||
        foundParentNode.locker.withWriteLock {
 | 
			
		||||
            val left = foundParentNode.leftNode
 | 
			
		||||
            val right = foundParentNode.rightNode
 | 
			
		||||
            when {
 | 
			
		||||
                left != null && left.comparator.compare(data, left.key) == 0 -> {
 | 
			
		||||
                    foundParentNode.leftNode = null
 | 
			
		||||
                    foundParentNode.onFoundToRemoveCallback(left.leftNode, left.rightNode)
 | 
			
		||||
                    return foundParentNode to left
 | 
			
		||||
                }
 | 
			
		||||
                right != null && right.comparator.compare(data, right.key) == 0 -> {
 | 
			
		||||
                    foundParentNode.rightNode = null
 | 
			
		||||
                    foundParentNode.onFoundToRemoveCallback(right.leftNode, right.rightNode)
 | 
			
		||||
                    return foundParentNode to right
 | 
			
		||||
                }
 | 
			
		||||
                else -> {
 | 
			
		||||
                    return@withWriteLock // data has been changed, new search required
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    error("Unable to remove node")
 | 
			
		||||
}
 | 
			
		||||
suspend fun <K, V> SortedMapLikeBinaryTreeNode<K, V>.findNode(key: K): SortedMapLikeBinaryTreeNode<K, V>? {
 | 
			
		||||
    var currentlyChecking: SortedMapLikeBinaryTreeNode<K, V>? = this
 | 
			
		||||
    val lockedLockers = mutableSetOf<SmartRWLocker>()
 | 
			
		||||
    try {
 | 
			
		||||
        while (coroutineContext.job.isActive) {
 | 
			
		||||
            if (currentlyChecking == null) {
 | 
			
		||||
                return null
 | 
			
		||||
            }
 | 
			
		||||
            if (currentlyChecking.locker !in lockedLockers) {
 | 
			
		||||
                currentlyChecking.locker.acquireRead()
 | 
			
		||||
                lockedLockers.add(currentlyChecking.locker)
 | 
			
		||||
            }
 | 
			
		||||
            val comparingResult = currentlyChecking.comparator.compare(key, currentlyChecking.key)
 | 
			
		||||
            when {
 | 
			
		||||
                comparingResult > 0 -> {
 | 
			
		||||
                    currentlyChecking = currentlyChecking.rightNode
 | 
			
		||||
                    continue
 | 
			
		||||
                }
 | 
			
		||||
                comparingResult < 0 -> {
 | 
			
		||||
                    currentlyChecking = currentlyChecking.leftNode
 | 
			
		||||
                    continue
 | 
			
		||||
                }
 | 
			
		||||
                else -> return currentlyChecking
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    } finally {
 | 
			
		||||
        lockedLockers.forEach {
 | 
			
		||||
            runCatching { it.releaseRead() }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    error("Unable to find node")
 | 
			
		||||
}
 | 
			
		||||
suspend fun <K, V> SortedMapLikeBinaryTreeNode<K, V>.contains(data: K): Boolean = findNode(data) != null
 | 
			
		||||
 | 
			
		||||
suspend fun <K, V> SortedMapLikeBinaryTreeNode<K, V>.findNodesInRange(from: K, to: K, fromInclusiveMode: Boolean, toInclusiveMode: Boolean): Set<SortedMapLikeBinaryTreeNode<K, V>> {
 | 
			
		||||
    val results = mutableSetOf<SortedMapLikeBinaryTreeNode<K, V>>()
 | 
			
		||||
    val leftToCheck = mutableSetOf(this)
 | 
			
		||||
    val lockedLockers = mutableSetOf<SmartRWLocker>()
 | 
			
		||||
    val fromComparingFun: (SortedMapLikeBinaryTreeNode<K, V>) -> Boolean = if (fromInclusiveMode) {
 | 
			
		||||
        { it.comparator.compare(from, it.key) <= 0 }
 | 
			
		||||
    } else {
 | 
			
		||||
        { it.comparator.compare(from, it.key) < 0 }
 | 
			
		||||
    }
 | 
			
		||||
    val toComparingFun: (SortedMapLikeBinaryTreeNode<K, V>) -> Boolean = if (toInclusiveMode) {
 | 
			
		||||
        { it.comparator.compare(to, it.key) >= 0 }
 | 
			
		||||
    } else {
 | 
			
		||||
        { it.comparator.compare(to, it.key) > 0 }
 | 
			
		||||
    }
 | 
			
		||||
    try {
 | 
			
		||||
        while (coroutineContext.job.isActive && leftToCheck.isNotEmpty()) {
 | 
			
		||||
            val currentlyChecking = leftToCheck.first()
 | 
			
		||||
            leftToCheck.remove(currentlyChecking)
 | 
			
		||||
            if (currentlyChecking in results) {
 | 
			
		||||
                continue
 | 
			
		||||
            }
 | 
			
		||||
            currentlyChecking.locker.acquireRead()
 | 
			
		||||
            lockedLockers.add(currentlyChecking.locker)
 | 
			
		||||
            if (fromComparingFun(currentlyChecking) && toComparingFun(currentlyChecking)) {
 | 
			
		||||
                results.add(currentlyChecking)
 | 
			
		||||
                currentlyChecking.leftNode ?.let { leftToCheck.add(it) }
 | 
			
		||||
                currentlyChecking.rightNode ?.let { leftToCheck.add(it) }
 | 
			
		||||
                continue
 | 
			
		||||
            }
 | 
			
		||||
            when {
 | 
			
		||||
                currentlyChecking.comparator.compare(to, currentlyChecking.key) < 0 -> currentlyChecking.leftNode ?.let { leftToCheck.add(it) }
 | 
			
		||||
                currentlyChecking.comparator.compare(from, currentlyChecking.key) > 0 -> currentlyChecking.rightNode ?.let { leftToCheck.add(it) }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return results.toSet()
 | 
			
		||||
    } finally {
 | 
			
		||||
        lockedLockers.forEach {
 | 
			
		||||
            runCatching { it.releaseRead() }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    error("Unable to find nodes range")
 | 
			
		||||
}
 | 
			
		||||
suspend fun <K, V> SortedMapLikeBinaryTreeNode<K, V>.deepEquals(other: SortedMapLikeBinaryTreeNode<K, V>): Boolean {
 | 
			
		||||
    val leftToCheck = mutableSetOf(this)
 | 
			
		||||
    val othersToCheck = mutableSetOf(other)
 | 
			
		||||
    val lockedLockers = mutableSetOf<SmartRWLocker>()
 | 
			
		||||
    try {
 | 
			
		||||
        while (leftToCheck.isNotEmpty() && othersToCheck.isNotEmpty()) {
 | 
			
		||||
            val thisToCheck = leftToCheck.first()
 | 
			
		||||
            leftToCheck.remove(thisToCheck)
 | 
			
		||||
 | 
			
		||||
            val otherToCheck = othersToCheck.first()
 | 
			
		||||
            othersToCheck.remove(otherToCheck)
 | 
			
		||||
 | 
			
		||||
            if (thisToCheck.locker !in lockedLockers) {
 | 
			
		||||
                thisToCheck.locker.acquireRead()
 | 
			
		||||
                lockedLockers.add(thisToCheck.locker)
 | 
			
		||||
            }
 | 
			
		||||
            if (otherToCheck.locker !in lockedLockers) {
 | 
			
		||||
                otherToCheck.locker.acquireRead()
 | 
			
		||||
                lockedLockers.add(otherToCheck.locker)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (thisToCheck.key != otherToCheck.key || thisToCheck.value != otherToCheck.value) {
 | 
			
		||||
                return false
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if ((thisToCheck.leftNode == null).xor(otherToCheck.leftNode == null)) {
 | 
			
		||||
                return false
 | 
			
		||||
            }
 | 
			
		||||
            if ((thisToCheck.rightNode == null).xor(otherToCheck.rightNode == null)) {
 | 
			
		||||
                return false
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            thisToCheck.leftNode?.let { leftToCheck.add(it) }
 | 
			
		||||
            thisToCheck.rightNode?.let { leftToCheck.add(it) }
 | 
			
		||||
 | 
			
		||||
            otherToCheck.leftNode?.let { othersToCheck.add(it) }
 | 
			
		||||
            otherToCheck.rightNode?.let { othersToCheck.add(it) }
 | 
			
		||||
        }
 | 
			
		||||
    } finally {
 | 
			
		||||
        lockedLockers.forEach {
 | 
			
		||||
            runCatching { it.releaseRead() }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return leftToCheck.isEmpty() && othersToCheck.isEmpty()
 | 
			
		||||
}
 | 
			
		||||
suspend fun <K, V> SortedMapLikeBinaryTreeNode<K, V>.findNodesInRange(from: K, to: K): Set<SortedMapLikeBinaryTreeNode<K, V>> = findNodesInRange(
 | 
			
		||||
    from = from,
 | 
			
		||||
    to = to,
 | 
			
		||||
    fromInclusiveMode = true,
 | 
			
		||||
    toInclusiveMode = true
 | 
			
		||||
)
 | 
			
		||||
suspend fun <K, V> SortedMapLikeBinaryTreeNode<K, V>.findNodesInRangeExcluding(from: K, to: K): Set<SortedMapLikeBinaryTreeNode<K, V>> = findNodesInRange(
 | 
			
		||||
    from = from,
 | 
			
		||||
    to = to,
 | 
			
		||||
    fromInclusiveMode = false,
 | 
			
		||||
    toInclusiveMode = false
 | 
			
		||||
)
 | 
			
		||||
suspend fun <K : Comparable<K>, V> SortedMapLikeBinaryTreeNode<K, V>.findNodesInRange(range: ClosedRange<K>): Set<SortedMapLikeBinaryTreeNode<K, V>> = findNodesInRange(
 | 
			
		||||
    from = range.start,
 | 
			
		||||
    to = range.endInclusive,
 | 
			
		||||
)
 | 
			
		||||
@@ -0,0 +1,118 @@
 | 
			
		||||
package dev.inmo.micro_utils.coroutines
 | 
			
		||||
 | 
			
		||||
import dev.inmo.micro_utils.coroutines.collections.*
 | 
			
		||||
import kotlinx.coroutines.test.runTest
 | 
			
		||||
import kotlin.test.Test
 | 
			
		||||
import kotlin.test.assertEquals
 | 
			
		||||
import kotlin.test.assertTrue
 | 
			
		||||
import kotlin.time.Duration.Companion.seconds
 | 
			
		||||
 | 
			
		||||
class SortedMapLikeBinaryTreeNodeTests {
 | 
			
		||||
    @Test
 | 
			
		||||
    fun insertOnZeroLevelWorks() = runTest {
 | 
			
		||||
        val zeroNode = SortedMapLikeBinaryTreeNode(0, 0)
 | 
			
		||||
        zeroNode.upsertSubNode(1, 1)
 | 
			
		||||
        zeroNode.upsertSubNode(-1, -1)
 | 
			
		||||
 | 
			
		||||
        assertEquals(0, zeroNode.key)
 | 
			
		||||
        assertEquals(1, zeroNode.getRightNode() ?.key)
 | 
			
		||||
        assertEquals(-1, zeroNode.getLeftNode() ?.key)
 | 
			
		||||
 | 
			
		||||
        assertEquals(0, zeroNode.findNode(0) ?.value)
 | 
			
		||||
        assertEquals(1, zeroNode.findNode(1) ?.value)
 | 
			
		||||
        assertEquals(-1, zeroNode.findNode(-1) ?.value)
 | 
			
		||||
    }
 | 
			
		||||
    @Test
 | 
			
		||||
    fun searchOnZeroLevelWorks() = runTest {
 | 
			
		||||
        val zeroNode = SortedMapLikeBinaryTreeNode(0, 0)
 | 
			
		||||
        val oneNode = zeroNode.upsertSubNode(1, 1)
 | 
			
		||||
        val minusOneNode = zeroNode.upsertSubNode(-1, -1)
 | 
			
		||||
 | 
			
		||||
        val assertingNodesToSearchQuery = mapOf(
 | 
			
		||||
            setOf(oneNode) to (1 .. 1),
 | 
			
		||||
            setOf(zeroNode, oneNode) to (0 .. 1),
 | 
			
		||||
            setOf(minusOneNode, zeroNode, oneNode) to (-1 .. 1),
 | 
			
		||||
            setOf(minusOneNode, zeroNode) to (-1 .. 0),
 | 
			
		||||
            setOf(minusOneNode) to (-1 .. -1),
 | 
			
		||||
            setOf(zeroNode) to (0 .. 0),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        assertingNodesToSearchQuery.forEach {
 | 
			
		||||
            val foundData = zeroNode.findNodesInRange(it.value)
 | 
			
		||||
            assertTrue(foundData.containsAll(it.key))
 | 
			
		||||
            assertTrue(it.key.containsAll(foundData))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    @Test
 | 
			
		||||
    fun deepReInsertOnWorks() = runTest(timeout = 300.seconds) {
 | 
			
		||||
        var zeroNode = SortedMapLikeBinaryTreeNode(0, 0)
 | 
			
		||||
        val rangeRadius = 500
 | 
			
		||||
        val nodes = mutableMapOf<Int, SortedMapLikeBinaryTreeNode<Int, Int>>()
 | 
			
		||||
        for (i in -rangeRadius .. rangeRadius) {
 | 
			
		||||
            nodes[i] = zeroNode.upsertSubNode(i, i)
 | 
			
		||||
            if (i == zeroNode.key) {
 | 
			
		||||
                zeroNode = nodes.getValue(i)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        for (i in -rangeRadius .. rangeRadius) {
 | 
			
		||||
            val expectedNode = nodes.getValue(i)
 | 
			
		||||
            val foundNode = zeroNode.findNode(i)
 | 
			
		||||
 | 
			
		||||
            assertEquals(expectedNode, foundNode)
 | 
			
		||||
 | 
			
		||||
            if (expectedNode === zeroNode) continue
 | 
			
		||||
 | 
			
		||||
            val parentNode = zeroNode.findParentNode(i)
 | 
			
		||||
            assertTrue(
 | 
			
		||||
                parentNode ?.getLeftNode() === expectedNode || parentNode ?.getRightNode() === expectedNode,
 | 
			
		||||
                "It is expected, that parent node with data ${parentNode ?.key} will be parent of ${expectedNode.key}, but its left subnode is ${parentNode ?.getLeftNode() ?.key} and right one is ${parentNode ?.getRightNode() ?.key}"
 | 
			
		||||
            )
 | 
			
		||||
            assertTrue(
 | 
			
		||||
                foundNode != null && expectedNode.deepEquals(foundNode)
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            zeroNode.upsertSubNode(i, -i)
 | 
			
		||||
            val foundModifiedNode = zeroNode.findNode(i)
 | 
			
		||||
            assertEquals(foundNode ?.value, foundModifiedNode ?.value ?.times(-1))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    @Test
 | 
			
		||||
    fun deepInsertOnWorks() = runTest(timeout = 240.seconds) {
 | 
			
		||||
        val zeroNode = SortedMapLikeBinaryTreeNode(0, 0)
 | 
			
		||||
        val rangeRadius = 500
 | 
			
		||||
        val nodes = mutableMapOf<Int, SortedMapLikeBinaryTreeNode<Int, Int>>()
 | 
			
		||||
        for (i in -rangeRadius .. rangeRadius) {
 | 
			
		||||
            if (zeroNode.key != i) {
 | 
			
		||||
                nodes[i] = zeroNode.upsertSubNode(i, i)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        nodes[zeroNode.key] = zeroNode
 | 
			
		||||
 | 
			
		||||
        for (i in -rangeRadius .. rangeRadius) {
 | 
			
		||||
            val expectedNode = nodes.getValue(i)
 | 
			
		||||
            val foundNode = zeroNode.findNode(i)
 | 
			
		||||
 | 
			
		||||
            assertTrue(expectedNode === foundNode)
 | 
			
		||||
 | 
			
		||||
            if (expectedNode === zeroNode) continue
 | 
			
		||||
 | 
			
		||||
            val parentNode = zeroNode.findParentNode(i)
 | 
			
		||||
            assertTrue(
 | 
			
		||||
                parentNode ?.getLeftNode() === expectedNode || parentNode ?.getRightNode() === expectedNode,
 | 
			
		||||
                "It is expected, that parent node with data ${parentNode ?.key} will be parent of ${expectedNode.key}, but its left subnode is ${parentNode ?.getLeftNode() ?.key} and right one is ${parentNode ?.getRightNode() ?.key}"
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val sourceTreeSize = zeroNode.size()
 | 
			
		||||
 | 
			
		||||
        var previousData = -rangeRadius - 1
 | 
			
		||||
        for (node in zeroNode) {
 | 
			
		||||
            assertTrue(nodes[node.key] === node)
 | 
			
		||||
            assertTrue(previousData == node.key - 1)
 | 
			
		||||
            previousData = node.key
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        assertTrue(sourceTreeSize == zeroNode.size())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -15,5 +15,5 @@ crypto_js_version=4.1.1
 | 
			
		||||
# Project data
 | 
			
		||||
 | 
			
		||||
group=dev.inmo
 | 
			
		||||
version=0.25.0
 | 
			
		||||
android_code_version=290
 | 
			
		||||
version=0.25.1
 | 
			
		||||
android_code_version=291
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,13 @@ package dev.inmo.micro_utils.pagination.compose
 | 
			
		||||
 | 
			
		||||
import androidx.compose.runtime.*
 | 
			
		||||
import dev.inmo.micro_utils.coroutines.SpecialMutableStateFlow
 | 
			
		||||
import dev.inmo.micro_utils.coroutines.launchLoggingDropExceptions
 | 
			
		||||
import dev.inmo.micro_utils.coroutines.runCatchingLogging
 | 
			
		||||
import dev.inmo.micro_utils.pagination.*
 | 
			
		||||
import kotlinx.coroutines.CoroutineScope
 | 
			
		||||
import kotlinx.coroutines.Job
 | 
			
		||||
import kotlinx.coroutines.sync.Mutex
 | 
			
		||||
import kotlinx.coroutines.sync.withLock
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Context for managing infinite pagination in a Compose UI.
 | 
			
		||||
@@ -16,30 +22,46 @@ import dev.inmo.micro_utils.pagination.*
 | 
			
		||||
 */
 | 
			
		||||
class InfinityPagedComponentContext<T> internal constructor(
 | 
			
		||||
    page: Int,
 | 
			
		||||
    size: Int
 | 
			
		||||
    size: Int,
 | 
			
		||||
    private val scope: CoroutineScope,
 | 
			
		||||
    private val loader: suspend InfinityPagedComponentContext<T>.(Pagination) -> PaginationResult<T>
 | 
			
		||||
) {
 | 
			
		||||
    internal val startPage = SimplePagination(page, size)
 | 
			
		||||
    internal val currentlyLoadingPage = SpecialMutableStateFlow<Pagination?>(startPage)
 | 
			
		||||
    internal val latestLoadedPage = SpecialMutableStateFlow<PaginationResult<T>?>(null)
 | 
			
		||||
    internal val dataState = SpecialMutableStateFlow<List<T>?>(null)
 | 
			
		||||
    internal var loadingJob: Job? = null
 | 
			
		||||
    internal val loadingMutex = Mutex()
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Loads the next page of data. If the current page is the last one, the function returns early.
 | 
			
		||||
     */
 | 
			
		||||
    fun loadNext() {
 | 
			
		||||
        if (latestLoadedPage.value ?.isLastPage == true) return
 | 
			
		||||
        if (currentlyLoadingPage.value != null) return // Data loading has been inited but not loaded yet
 | 
			
		||||
 | 
			
		||||
        currentlyLoadingPage.value = latestLoadedPage.value ?.nextPage() ?: startPage
 | 
			
		||||
    fun loadNext(): Job {
 | 
			
		||||
        return scope.launchLoggingDropExceptions {
 | 
			
		||||
            loadingMutex.withLock {
 | 
			
		||||
                if (latestLoadedPage.value ?.isLastPage == true) return@launchLoggingDropExceptions
 | 
			
		||||
                loadingJob = loadingJob ?: scope.launchLoggingDropExceptions {
 | 
			
		||||
                    runCatching {
 | 
			
		||||
                        loader(latestLoadedPage.value ?.nextPage() ?: startPage)
 | 
			
		||||
                    }.onSuccess {
 | 
			
		||||
                        latestLoadedPage.value = it
 | 
			
		||||
                        dataState.value = (dataState.value ?: emptyList()) + it.results
 | 
			
		||||
                    }
 | 
			
		||||
                    loadingMutex.withLock {
 | 
			
		||||
                        loadingJob = null
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                loadingJob
 | 
			
		||||
            } ?.join()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Reloads the pagination from the first page, clearing previously loaded data.
 | 
			
		||||
     */
 | 
			
		||||
    fun reload() {
 | 
			
		||||
    fun reload(): Job {
 | 
			
		||||
        latestLoadedPage.value = null
 | 
			
		||||
        currentlyLoadingPage.value = null
 | 
			
		||||
        loadNext()
 | 
			
		||||
        dataState.value = null
 | 
			
		||||
        return loadNext()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -58,17 +80,13 @@ internal fun <T> InfinityPagedComponent(
 | 
			
		||||
    page: Int,
 | 
			
		||||
    size: Int,
 | 
			
		||||
    loader: suspend InfinityPagedComponentContext<T>.(Pagination) -> PaginationResult<T>,
 | 
			
		||||
    predefinedScope: CoroutineScope? = null,
 | 
			
		||||
    block: @Composable InfinityPagedComponentContext<T>.(List<T>?) -> Unit
 | 
			
		||||
) {
 | 
			
		||||
    val context = remember { InfinityPagedComponentContext<T>(page, size) }
 | 
			
		||||
 | 
			
		||||
    val currentlyLoadingState = context.currentlyLoadingPage.collectAsState()
 | 
			
		||||
    LaunchedEffect(currentlyLoadingState.value) {
 | 
			
		||||
        val paginationResult = loader(context, currentlyLoadingState.value ?: return@LaunchedEffect)
 | 
			
		||||
        context.latestLoadedPage.value = paginationResult
 | 
			
		||||
        context.currentlyLoadingPage.value = null
 | 
			
		||||
 | 
			
		||||
        context.dataState.value = (context.dataState.value ?: emptyList()) + paginationResult.results
 | 
			
		||||
    val scope = predefinedScope ?: rememberCoroutineScope()
 | 
			
		||||
    val context = remember { InfinityPagedComponentContext<T>(page, size, scope, loader) }
 | 
			
		||||
    remember {
 | 
			
		||||
        context.reload()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    val dataState = context.dataState.collectAsState()
 | 
			
		||||
@@ -88,12 +106,14 @@ internal fun <T> InfinityPagedComponent(
 | 
			
		||||
fun <T> InfinityPagedComponent(
 | 
			
		||||
    pageInfo: Pagination,
 | 
			
		||||
    loader: suspend InfinityPagedComponentContext<T>.(Pagination) -> PaginationResult<T>,
 | 
			
		||||
    predefinedScope: CoroutineScope? = null,
 | 
			
		||||
    block: @Composable InfinityPagedComponentContext<T>.(List<T>?) -> Unit
 | 
			
		||||
) {
 | 
			
		||||
    InfinityPagedComponent(
 | 
			
		||||
        pageInfo.page,
 | 
			
		||||
        pageInfo.size,
 | 
			
		||||
        loader,
 | 
			
		||||
        predefinedScope,
 | 
			
		||||
        block
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
@@ -111,7 +131,8 @@ fun <T> InfinityPagedComponent(
 | 
			
		||||
fun <T> InfinityPagedComponent(
 | 
			
		||||
    size: Int,
 | 
			
		||||
    loader: suspend InfinityPagedComponentContext<T>.(Pagination) -> PaginationResult<T>,
 | 
			
		||||
    predefinedScope: CoroutineScope? = null,
 | 
			
		||||
    block: @Composable InfinityPagedComponentContext<T>.(List<T>?) -> Unit
 | 
			
		||||
) {
 | 
			
		||||
    InfinityPagedComponent(0, size, loader, block)
 | 
			
		||||
    InfinityPagedComponent(0, size, loader, predefinedScope, block)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,12 @@ package dev.inmo.micro_utils.pagination.compose
 | 
			
		||||
 | 
			
		||||
import androidx.compose.runtime.*
 | 
			
		||||
import dev.inmo.micro_utils.coroutines.SpecialMutableStateFlow
 | 
			
		||||
import dev.inmo.micro_utils.coroutines.launchLoggingDropExceptions
 | 
			
		||||
import dev.inmo.micro_utils.pagination.*
 | 
			
		||||
import kotlinx.coroutines.CoroutineScope
 | 
			
		||||
import kotlinx.coroutines.Job
 | 
			
		||||
import kotlinx.coroutines.sync.Mutex
 | 
			
		||||
import kotlinx.coroutines.sync.withLock
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Context for managing paginated data in a Compose UI.
 | 
			
		||||
@@ -18,39 +23,73 @@ import dev.inmo.micro_utils.pagination.*
 | 
			
		||||
 */
 | 
			
		||||
class PagedComponentContext<T> internal constructor(
 | 
			
		||||
    initialPage: Int,
 | 
			
		||||
    size: Int
 | 
			
		||||
    size: Int,
 | 
			
		||||
    private val scope: CoroutineScope,
 | 
			
		||||
    private val loader: suspend PagedComponentContext<T>.(Pagination) -> PaginationResult<T>
 | 
			
		||||
) {
 | 
			
		||||
    internal val startPage = SimplePagination(initialPage, size)
 | 
			
		||||
    internal val currentlyLoadingPageState = SpecialMutableStateFlow<Pagination?>(startPage)
 | 
			
		||||
    internal val latestLoadedPage = SpecialMutableStateFlow<PaginationResult<T>?>(null)
 | 
			
		||||
    internal val dataState = SpecialMutableStateFlow<PaginationResult<T>?>(null)
 | 
			
		||||
    internal var loadingJob: Job? = null
 | 
			
		||||
    internal val loadingMutex = Mutex()
 | 
			
		||||
 | 
			
		||||
    private fun initLoadingJob(
 | 
			
		||||
        skipCheckerInLock: () -> Boolean,
 | 
			
		||||
        pageGetter: () -> Pagination
 | 
			
		||||
    ): Job {
 | 
			
		||||
        return scope.launchLoggingDropExceptions {
 | 
			
		||||
            loadingMutex.withLock {
 | 
			
		||||
                if (skipCheckerInLock()) return@launchLoggingDropExceptions
 | 
			
		||||
                loadingJob = loadingJob ?: scope.launchLoggingDropExceptions {
 | 
			
		||||
                    runCatching {
 | 
			
		||||
                        loader(pageGetter())
 | 
			
		||||
                    }.onSuccess {
 | 
			
		||||
                        latestLoadedPage.value = it
 | 
			
		||||
                        dataState.value = it
 | 
			
		||||
                    }
 | 
			
		||||
                    loadingMutex.withLock {
 | 
			
		||||
                        loadingJob = null
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                loadingJob
 | 
			
		||||
            } ?.join()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Loads the next page of data. If the last page is reached, this function returns early.
 | 
			
		||||
     */
 | 
			
		||||
    fun loadNext() {
 | 
			
		||||
        when {
 | 
			
		||||
            currentlyLoadingPageState.value != null -> return
 | 
			
		||||
            latestLoadedPage.value ?.isLastPage == true -> return
 | 
			
		||||
            else -> currentlyLoadingPageState.value = (latestLoadedPage.value ?.nextPage()) ?: startPage
 | 
			
		||||
    fun loadNext(): Job {
 | 
			
		||||
        return initLoadingJob(
 | 
			
		||||
            { latestLoadedPage.value ?.isLastPage == true }
 | 
			
		||||
        ) {
 | 
			
		||||
            latestLoadedPage.value ?.nextPage() ?: startPage
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Loads the previous page of data if available.
 | 
			
		||||
     */
 | 
			
		||||
    fun loadPrevious() {
 | 
			
		||||
        when {
 | 
			
		||||
            currentlyLoadingPageState.value != null -> return
 | 
			
		||||
            latestLoadedPage.value ?.isFirstPage == true -> return
 | 
			
		||||
            else -> currentlyLoadingPageState.value = (latestLoadedPage.value ?.previousPage()) ?: startPage
 | 
			
		||||
    fun loadPrevious(): Job {
 | 
			
		||||
        return initLoadingJob(
 | 
			
		||||
            { latestLoadedPage.value ?.isFirstPage == true }
 | 
			
		||||
        ) {
 | 
			
		||||
            latestLoadedPage.value ?.previousPage() ?: startPage
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Reloads the current page, refreshing the data.
 | 
			
		||||
     */
 | 
			
		||||
    fun reload() {
 | 
			
		||||
        currentlyLoadingPageState.value = latestLoadedPage.value
 | 
			
		||||
    fun reload(): Job {
 | 
			
		||||
        return initLoadingJob(
 | 
			
		||||
            {
 | 
			
		||||
                latestLoadedPage.value = null
 | 
			
		||||
                true
 | 
			
		||||
            }
 | 
			
		||||
        ) {
 | 
			
		||||
            startPage
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -69,18 +108,16 @@ internal fun <T> PagedComponent(
 | 
			
		||||
    initialPage: Int,
 | 
			
		||||
    size: Int,
 | 
			
		||||
    loader: suspend PagedComponentContext<T>.(Pagination) -> PaginationResult<T>,
 | 
			
		||||
    predefinedScope: CoroutineScope? = null,
 | 
			
		||||
    block: @Composable PagedComponentContext<T>.(PaginationResult<T>) -> Unit
 | 
			
		||||
) {
 | 
			
		||||
    val context = remember { PagedComponentContext<T>(initialPage, size) }
 | 
			
		||||
 | 
			
		||||
    val currentlyLoadingState = context.currentlyLoadingPageState.collectAsState()
 | 
			
		||||
    LaunchedEffect(currentlyLoadingState.value) {
 | 
			
		||||
        val paginationResult = loader(context, currentlyLoadingState.value ?: return@LaunchedEffect)
 | 
			
		||||
        context.latestLoadedPage.value = paginationResult
 | 
			
		||||
        context.currentlyLoadingPageState.value = null
 | 
			
		||||
    val scope = predefinedScope ?: rememberCoroutineScope()
 | 
			
		||||
    val context = remember { PagedComponentContext<T>(initialPage, size, scope, loader) }
 | 
			
		||||
    remember {
 | 
			
		||||
        context.reload()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    val pageState = context.latestLoadedPage.collectAsState()
 | 
			
		||||
    val pageState = context.dataState.collectAsState()
 | 
			
		||||
    pageState.value ?.let {
 | 
			
		||||
        context.block(it)
 | 
			
		||||
    }
 | 
			
		||||
@@ -98,12 +135,14 @@ internal fun <T> PagedComponent(
 | 
			
		||||
fun <T> PagedComponent(
 | 
			
		||||
    pageInfo: Pagination,
 | 
			
		||||
    loader: suspend PagedComponentContext<T>.(Pagination) -> PaginationResult<T>,
 | 
			
		||||
    predefinedScope: CoroutineScope? = null,
 | 
			
		||||
    block: @Composable PagedComponentContext<T>.(PaginationResult<T>) -> Unit
 | 
			
		||||
) {
 | 
			
		||||
    PagedComponent(
 | 
			
		||||
        pageInfo.page,
 | 
			
		||||
        pageInfo.size,
 | 
			
		||||
        loader,
 | 
			
		||||
        predefinedScope,
 | 
			
		||||
        block
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
@@ -120,7 +159,8 @@ fun <T> PagedComponent(
 | 
			
		||||
fun <T> PagedComponent(
 | 
			
		||||
    size: Int,
 | 
			
		||||
    loader: suspend PagedComponentContext<T>.(Pagination) -> PaginationResult<T>,
 | 
			
		||||
    predefinedScope: CoroutineScope? = null,
 | 
			
		||||
    block: @Composable PagedComponentContext<T>.(PaginationResult<T>) -> Unit
 | 
			
		||||
) {
 | 
			
		||||
    PagedComponent(0, size, loader, block)
 | 
			
		||||
    PagedComponent(0, size, loader, predefinedScope, block)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -30,13 +30,13 @@ class InfinityPagedComponentTests {
 | 
			
		||||
                }
 | 
			
		||||
            ) {
 | 
			
		||||
                if (it == null) {
 | 
			
		||||
                    assertEquals(0, this.currentlyLoadingPage.value ?.page)
 | 
			
		||||
                    assertEquals(null, it)
 | 
			
		||||
                } else {
 | 
			
		||||
                    assertEquals(expectedList, it)
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                LaunchedEffect(it ?.size) {
 | 
			
		||||
                    loadNext()
 | 
			
		||||
                    loadNext().join()
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user