diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 1fab347ad..a5504a4a3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -179,6 +179,7 @@ import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent import com.vitorpamplona.quartz.nipB7Blossom.BlossomServersEvent import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.LargeCache +import com.vitorpamplona.quartz.utils.LargeSoftCache import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @@ -208,9 +209,9 @@ interface ILocalCache { object LocalCache : ILocalCache { val antiSpam = AntiSpamFilter() - val users = LargeCache() - val notes = LargeCache() - val addressables = LargeCache() + val users = LargeSoftCache() + val notes = LargeSoftCache() + val addressables = LargeSoftCache() val chatroomList = LargeCache() val publicChatChannels = LargeCache() @@ -2163,6 +2164,12 @@ object LocalCache : ILocalCache { return newNotes } + fun cleanMemory() { + notes.cleanUp() + addressables.cleanUp() + users.cleanUp() + } + fun cleanObservers() { notes.forEach { _, it -> it.clearFlow() } addressables.forEach { _, it -> it.clearFlow() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/MediaAspectRatioCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/MediaAspectRatioCache.kt index dcf2401ef..b4b199f9d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/MediaAspectRatioCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/MediaAspectRatioCache.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.model import android.util.LruCache interface MutableMediaAspectRatioCache { - fun get(url: String): Float + fun get(url: String): Float? fun add( url: String, @@ -35,7 +35,7 @@ interface MutableMediaAspectRatioCache { object MediaAspectRatioCache : MutableMediaAspectRatioCache { val mediaAspectRatioCacheByUrl = LruCache(1000) - override fun get(url: String) = mediaAspectRatioCacheByUrl.get(url) + override fun get(url: String): Float? = mediaAspectRatioCacheByUrl.get(url) override fun add( url: String, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/eventCache/MemoryTrimmingService.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/eventCache/MemoryTrimmingService.kt index 1006244a5..6d31d5578 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/eventCache/MemoryTrimmingService.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/eventCache/MemoryTrimmingService.kt @@ -36,6 +36,7 @@ class MemoryTrimmingService( account: Account? = null, otherAccounts: List, ) { + cache.cleanMemory() cache.cleanObservers() account?.let { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/utils/LargeCache.kt b/quartz/src/main/java/com/vitorpamplona/quartz/utils/LargeCache.kt index a1bd268bc..bd61c05d1 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/utils/LargeCache.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/utils/LargeCache.kt @@ -170,25 +170,25 @@ class LargeCache { return runner.count } - public fun associate(transform: (K, V) -> Pair): Map { + fun associate(transform: (K, V) -> Pair): Map { val runner = BiAssociateCollector(size(), transform) innerForEach(runner) return runner.results } - public fun associateNotNull(transform: (K, V) -> Pair?): Map { + fun associateNotNull(transform: (K, V) -> Pair?): Map { val runner = BiAssociateNotNullCollector(size(), transform) innerForEach(runner) return runner.results } - public fun associateWith(transform: (K, V) -> U?): Map { + fun associateWith(transform: (K, V) -> U?): Map { val runner = BiAssociateWithCollector(size(), transform) innerForEach(runner) return runner.results } - public fun associateNotNullWith(transform: (K, V) -> U): Map { + fun associateNotNullWith(transform: (K, V) -> U): Map { val runner = BiAssociateNotNullWithCollector(size(), transform) innerForEach(runner) return runner.results @@ -227,338 +227,338 @@ class LargeCache { buffer.append(postfix) return buffer.toString() } -} -fun interface BiFilter { - fun filter( - k: K, - v: V, - ): Boolean -} - -class BiFilterCollector( - val filter: BiFilter, -) : BiConsumer { - var results: ArrayList = ArrayList() - - override fun accept( - k: K, - v: V, - ) { - if (filter.filter(k, v)) { - results.add(v) - } + fun interface BiFilter { + fun filter( + k: K, + v: V, + ): Boolean } -} -class BiFilterUniqueCollector( - val filter: BiFilter, -) : BiConsumer { - var results: HashSet = HashSet() + class BiFilterCollector( + val filter: BiFilter, + ) : BiConsumer { + var results: ArrayList = ArrayList() - override fun accept( - k: K, - v: V, - ) { - if (filter.filter(k, v)) { - results.add(v) - } - } -} - -fun interface BiMapper { - fun map( - k: K, - v: V, - ): R? -} - -fun interface BiMapperNotNull { - fun map( - k: K, - v: V, - ): R -} - -class BiMapCollector( - val mapper: BiMapper, -) : BiConsumer { - var results: ArrayList = ArrayList() - - override fun accept( - k: K, - v: V, - ) { - val result = mapper.map(k, v) - if (result != null) { - results.add(result) - } - } -} - -class BiAssociateCollector( - val size: Int, - val mapper: BiMapperNotNull>, -) : BiConsumer { - var results: LinkedHashMap = LinkedHashMap(size) - - override fun accept( - k: K, - v: V, - ) { - val pair = mapper.map(k, v) - results.put(pair.first, pair.second) - } -} - -class BiAssociateNotNullCollector( - val size: Int, - val mapper: BiMapper?>, -) : BiConsumer { - var results: LinkedHashMap = LinkedHashMap(size) - - override fun accept( - k: K, - v: V, - ) { - val pair = mapper.map(k, v) - if (pair != null) { - results.put(pair.first, pair.second) - } - } -} - -class BiAssociateWithCollector( - val size: Int, - val mapper: BiMapper, -) : BiConsumer { - var results: LinkedHashMap = LinkedHashMap(size) - - override fun accept( - k: K, - v: V, - ) { - results.put(k, mapper.map(k, v)) - } -} - -class BiAssociateNotNullWithCollector( - val size: Int, - val mapper: BiMapper, -) : BiConsumer { - var results: LinkedHashMap = LinkedHashMap(size) - - override fun accept( - k: K, - v: V, - ) { - val newValue = mapper.map(k, v) - if (newValue != null) { - results.put(k, newValue) - } - } -} - -class BiMapUniqueCollector( - val mapper: BiMapper, -) : BiConsumer { - var results: HashSet = HashSet() - - override fun accept( - k: K, - v: V, - ) { - val result = mapper.map(k, v) - if (result != null) { - results.add(result) - } - } -} - -class BiMapFlattenCollector( - val mapper: BiMapper?>, -) : BiConsumer { - var results: ArrayList = ArrayList() - - override fun accept( - k: K, - v: V, - ) { - val result = mapper.map(k, v) - if (result != null) { - results.addAll(result) - } - } -} - -class BiMapFlattenUniqueCollector( - val mapper: BiMapper?>, -) : BiConsumer { - var results: HashSet = HashSet() - - override fun accept( - k: K, - v: V, - ) { - val result = mapper.map(k, v) - if (result != null) { - results.addAll(result) - } - } -} - -fun interface BiNotNullMapper { - fun map( - k: K, - v: V, - ): R -} - -class BiNotNullMapCollector( - val mapper: BiNotNullMapper, -) : BiConsumer { - var results: ArrayList = ArrayList() - - override fun accept( - k: K, - v: V, - ) { - results.add(mapper.map(k, v)) - } -} - -fun interface BiSumOf { - fun map( - k: K, - v: V, - ): Int -} - -class BiMaxOfCollector( - val filter: BiFilter, - val comparator: Comparator, -) : BiConsumer { - var maxK: K? = null - var maxV: V? = null - - override fun accept( - k: K, - v: V, - ) { - if (filter.filter(k, v)) { - if (maxK == null || comparator.compare(v, maxV) > 0) { - maxK = k - maxV = v + override fun accept( + k: K, + v: V, + ) { + if (filter.filter(k, v)) { + results.add(v) } } } -} -class BiSumOfCollector( - val mapper: BiSumOf, -) : BiConsumer { - var sum = 0 + class BiFilterUniqueCollector( + val filter: BiFilter, + ) : BiConsumer { + var results: HashSet = HashSet() - override fun accept( - k: K, - v: V, - ) { - sum += mapper.map(k, v) + override fun accept( + k: K, + v: V, + ) { + if (filter.filter(k, v)) { + results.add(v) + } + } } -} -fun interface BiSumOfLong { - fun map( - k: K, - v: V, - ): Long -} - -class BiSumOfLongCollector( - val mapper: BiSumOfLong, -) : BiConsumer { - var sum = 0L - - override fun accept( - k: K, - v: V, - ) { - sum += mapper.map(k, v) + fun interface BiMapper { + fun map( + k: K, + v: V, + ): R? } -} -class BiGroupByCollector( - val mapper: BiNotNullMapper, -) : BiConsumer { - var results = HashMap>() + fun interface BiMapperNotNull { + fun map( + k: K, + v: V, + ): R + } - override fun accept( - k: K, - v: V, - ) { - val group = mapper.map(k, v) + class BiMapCollector( + val mapper: BiMapper, + ) : BiConsumer { + var results: ArrayList = ArrayList() - val list = results[group] - if (list == null) { - val answer = ArrayList() - answer.add(v) - results[group] = answer - } else { - list.add(v) + override fun accept( + k: K, + v: V, + ) { + val result = mapper.map(k, v) + if (result != null) { + results.add(result) + } + } + } + + class BiAssociateCollector( + val size: Int, + val mapper: BiMapperNotNull>, + ) : BiConsumer { + var results: LinkedHashMap = LinkedHashMap(size) + + override fun accept( + k: K, + v: V, + ) { + val pair = mapper.map(k, v) + results.put(pair.first, pair.second) + } + } + + class BiAssociateNotNullCollector( + val size: Int, + val mapper: BiMapper?>, + ) : BiConsumer { + var results: LinkedHashMap = LinkedHashMap(size) + + override fun accept( + k: K, + v: V, + ) { + val pair = mapper.map(k, v) + if (pair != null) { + results.put(pair.first, pair.second) + } + } + } + + class BiAssociateWithCollector( + val size: Int, + val mapper: BiMapper, + ) : BiConsumer { + var results: LinkedHashMap = LinkedHashMap(size) + + override fun accept( + k: K, + v: V, + ) { + results.put(k, mapper.map(k, v)) + } + } + + class BiAssociateNotNullWithCollector( + val size: Int, + val mapper: BiMapper, + ) : BiConsumer { + var results: LinkedHashMap = LinkedHashMap(size) + + override fun accept( + k: K, + v: V, + ) { + val newValue = mapper.map(k, v) + if (newValue != null) { + results.put(k, newValue) + } + } + } + + class BiMapUniqueCollector( + val mapper: BiMapper, + ) : BiConsumer { + var results: HashSet = HashSet() + + override fun accept( + k: K, + v: V, + ) { + val result = mapper.map(k, v) + if (result != null) { + results.add(result) + } + } + } + + class BiMapFlattenCollector( + val mapper: BiMapper?>, + ) : BiConsumer { + var results: ArrayList = ArrayList() + + override fun accept( + k: K, + v: V, + ) { + val result = mapper.map(k, v) + if (result != null) { + results.addAll(result) + } + } + } + + class BiMapFlattenUniqueCollector( + val mapper: BiMapper?>, + ) : BiConsumer { + var results: HashSet = HashSet() + + override fun accept( + k: K, + v: V, + ) { + val result = mapper.map(k, v) + if (result != null) { + results.addAll(result) + } + } + } + + fun interface BiNotNullMapper { + fun map( + k: K, + v: V, + ): R + } + + class BiNotNullMapCollector( + val mapper: BiNotNullMapper, + ) : BiConsumer { + var results: ArrayList = ArrayList() + + override fun accept( + k: K, + v: V, + ) { + results.add(mapper.map(k, v)) + } + } + + fun interface BiSumOf { + fun map( + k: K, + v: V, + ): Int + } + + class BiMaxOfCollector( + val filter: BiFilter, + val comparator: Comparator, + ) : BiConsumer { + var maxK: K? = null + var maxV: V? = null + + override fun accept( + k: K, + v: V, + ) { + if (filter.filter(k, v)) { + if (maxK == null || comparator.compare(v, maxV) > 0) { + maxK = k + maxV = v + } + } + } + } + + class BiSumOfCollector( + val mapper: BiSumOf, + ) : BiConsumer { + var sum = 0 + + override fun accept( + k: K, + v: V, + ) { + sum += mapper.map(k, v) + } + } + + fun interface BiSumOfLong { + fun map( + k: K, + v: V, + ): Long + } + + class BiSumOfLongCollector( + val mapper: BiSumOfLong, + ) : BiConsumer { + var sum = 0L + + override fun accept( + k: K, + v: V, + ) { + sum += mapper.map(k, v) + } + } + + class BiGroupByCollector( + val mapper: BiNotNullMapper, + ) : BiConsumer { + var results = HashMap>() + + override fun accept( + k: K, + v: V, + ) { + val group = mapper.map(k, v) + + val list = results[group] + if (list == null) { + val answer = ArrayList() + answer.add(v) + results[group] = answer + } else { + list.add(v) + } + } + } + + class BiCountByGroupCollector( + val mapper: BiNotNullMapper, + ) : BiConsumer { + var results = HashMap() + + override fun accept( + k: K, + v: V, + ) { + val group = mapper.map(k, v) + + val count = results[group] + if (count == null) { + results[group] = 1 + } else { + results[group] = count + 1 + } + } + } + + class BiSumByGroupCollector( + val mapper: BiNotNullMapper, + val sumOf: BiNotNullMapper, + ) : BiConsumer { + var results = HashMap() + + override fun accept( + k: K, + v: V, + ) { + val group = mapper.map(k, v) + + val sum = results[group] + if (sum == null) { + results[group] = sumOf.map(k, v) + } else { + results[group] = sum + sumOf.map(k, v) + } + } + } + + class BiCountIfCollector( + val filter: BiFilter, + ) : BiConsumer { + var count = 0 + + override fun accept( + k: K, + v: V, + ) { + if (filter.filter(k, v)) count++ } } } - -class BiCountByGroupCollector( - val mapper: BiNotNullMapper, -) : BiConsumer { - var results = HashMap() - - override fun accept( - k: K, - v: V, - ) { - val group = mapper.map(k, v) - - val count = results[group] - if (count == null) { - results[group] = 1 - } else { - results[group] = count + 1 - } - } -} - -class BiSumByGroupCollector( - val mapper: BiNotNullMapper, - val sumOf: BiNotNullMapper, -) : BiConsumer { - var results = HashMap() - - override fun accept( - k: K, - v: V, - ) { - val group = mapper.map(k, v) - - val sum = results[group] - if (sum == null) { - results[group] = sumOf.map(k, v) - } else { - results[group] = sum + sumOf.map(k, v) - } - } -} - -class BiCountIfCollector( - val filter: BiFilter, -) : BiConsumer { - var count = 0 - - override fun accept( - k: K, - v: V, - ) { - if (filter.filter(k, v)) count++ - } -} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/utils/LargeSoftCache.kt b/quartz/src/main/java/com/vitorpamplona/quartz/utils/LargeSoftCache.kt new file mode 100644 index 000000000..50f539bd6 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/utils/LargeSoftCache.kt @@ -0,0 +1,627 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.utils + +import java.lang.ref.WeakReference +import java.util.concurrent.ConcurrentSkipListMap +import java.util.function.BiConsumer + +class LargeSoftCache { + private val cache = ConcurrentSkipListMap>() + + fun keys() = cache.keys + + fun get(key: K): V? { + val softRef = cache.get(key) + val value = softRef?.get() + + return if (value != null) { + value + } else { + cache.remove(key) + null + } + } + + fun remove(key: K) = cache.remove(key) + + fun size() = cache.size + + fun isEmpty() = cache.isEmpty() + + fun clear() = cache.clear() + + fun containsKey(key: K) = cache.containsKey(key) + + /** + * Puts an object into the cache with a specified key. + * The object is stored as a SoftReference. + * + * @param key The key to associate with the object. + * @param value The object to cache. + */ + fun put( + key: K, + value: V, + ) { + cache.put(key, WeakReference(value)) + } + + /** + * Retrieves an object from the cache using its key. + * Returns the object if it's still available (not garbage collected), + * otherwise returns null. If the object has been garbage collected, + * its entry is also removed from the cache. + * + * @param key The key of the object to retrieve. + * @return The cached object, or null if it's no longer available. + */ + fun getOrCreate( + key: K, + builder: (key: K) -> V, + ): V { + val softRef = cache.get(key) + val value = softRef?.get() + + return if (value != null) { + value + } else { + val newObject = builder(key) + cache.putIfAbsent(key, WeakReference(newObject))?.get() ?: newObject + } + } + + fun createIfAbsent( + key: K, + builder: (key: K) -> V, + ): Boolean { + val softRef = cache.get(key) + val value = softRef?.get() + return if (value != null) { + false + } else { + val newObject = builder(key) + cache.putIfAbsent(key, WeakReference(newObject)) == null + } + } + + /** + * Proactively cleans up the cache by removing entries whose weakly referenced + * objects have been garbage collected. While `get` handles cleanup on access, + * this method can be called periodically or when memory pressure is high. + */ + fun cleanUp() { + val keysToRemove = mutableListOf() + forEach { key, softRef -> + if (softRef == null) { + keysToRemove.add(key) + } + } + keysToRemove.forEach { key -> + cache.remove(key) + println("Cleaned up entry for key: $key (object was garbage collected)") + } + println("Cache cleanup completed. Remaining size: ${cache.size}") + } + + fun forEach(consumer: BiConsumer) { + innerForEach(consumer) + } + + fun filter(consumer: BiFilter): List { + val runner = BiFilterCollector(consumer) + innerForEach(runner) + return runner.results + } + + fun filterIntoSet(consumer: BiFilter): Set { + val runner = BiFilterUniqueCollector(consumer) + innerForEach(runner) + return runner.results + } + + fun map(consumer: BiNotNullMapper): List { + val runner = BiNotNullMapCollector(consumer) + innerForEach(runner) + return runner.results + } + + fun mapNotNull(consumer: BiMapper): List { + val runner = BiMapCollector(consumer) + innerForEach(runner) + return runner.results + } + + fun mapNotNullIntoSet(consumer: BiMapper): Set { + val runner = BiMapUniqueCollector(consumer) + innerForEach(runner) + return runner.results + } + + fun mapFlatten(consumer: BiMapper?>): List { + val runner = BiMapFlattenCollector(consumer) + innerForEach(runner) + return runner.results + } + + fun mapFlattenIntoSet(consumer: BiMapper?>): Set { + val runner = BiMapFlattenUniqueCollector(consumer) + innerForEach(runner) + return runner.results + } + + fun maxOrNullOf( + filter: BiFilter, + comparator: Comparator, + ): V? { + val runner = BiMaxOfCollector(filter, comparator) + innerForEach(runner) + return runner.maxV + } + + fun sumOf(consumer: BiSumOf): Int { + val runner = BiSumOfCollector(consumer) + innerForEach(runner) + return runner.sum + } + + fun sumOfLong(consumer: BiSumOfLong): Long { + val runner = BiSumOfLongCollector(consumer) + innerForEach(runner) + return runner.sum + } + + fun groupBy(consumer: BiNotNullMapper): Map> { + val runner = BiGroupByCollector(consumer) + innerForEach(runner) + return runner.results + } + + fun countByGroup(consumer: BiNotNullMapper): Map { + val runner = BiCountByGroupCollector(consumer) + innerForEach(runner) + return runner.results + } + + fun sumByGroup( + groupMap: BiNotNullMapper, + sumOf: BiNotNullMapper, + ): Map { + val runner = BiSumByGroupCollector(groupMap, sumOf) + innerForEach(runner) + return runner.results + } + + fun count(consumer: BiFilter): Int { + val runner = BiCountIfCollector(consumer) + innerForEach(runner) + return runner.count + } + + fun associate(transform: (K, V) -> Pair): Map { + val runner = BiAssociateCollector(size(), transform) + innerForEach(runner) + return runner.results + } + + fun associateNotNull(transform: (K, V) -> Pair?): Map { + val runner = BiAssociateNotNullCollector(size(), transform) + innerForEach(runner) + return runner.results + } + + fun associateWith(transform: (K, V) -> U?): Map { + val runner = BiAssociateWithCollector(size(), transform) + innerForEach(runner) + return runner.results + } + + fun associateNotNullWith(transform: (K, V) -> U): Map { + val runner = BiAssociateNotNullWithCollector(size(), transform) + innerForEach(runner) + return runner.results + } + + private fun innerForEach(runner: BiConsumer) { + cache.forEach(BiConsumerWrapper(this, runner)) + } + + fun joinToString( + separator: CharSequence = ", ", + prefix: CharSequence = "", + postfix: CharSequence = "", + limit: Int = -1, + truncated: CharSequence = "...", + transform: ((K, V) -> CharSequence)? = null, + ): String { + val buffer = StringBuilder() + buffer.append(prefix) + var count = 0 + forEach { key, value -> + val str = if (transform != null) transform(key, value) else "" + if (str.isNotEmpty()) { + if (++count > 1) buffer.append(separator) + if (limit < 0 || count <= limit) { + when { + transform != null -> buffer.append(str) + else -> buffer.append("$key $value") + } + } else { + return@forEach + } + } + } + if (limit >= 0 && count > limit) buffer.append(truncated) + buffer.append(postfix) + return buffer.toString() + } + + class BiConsumerWrapper( + val cache: LargeSoftCache, + val inner: BiConsumer, + ) : BiConsumer> { + override fun accept( + k: K, + ref: WeakReference, + ) { + val value = ref.get() + if (value == null) { + cache.remove(k) + } else { + inner.accept(k, value) + } + } + } + + fun interface BiFilter { + fun filter( + k: K, + v: V, + ): Boolean + } + + class BiFilterCollector( + val filter: BiFilter, + ) : BiConsumer { + var results: ArrayList = ArrayList() + + override fun accept( + k: K, + v: V, + ) { + if (filter.filter(k, v)) { + results.add(v) + } + } + } + + class BiFilterUniqueCollector( + val filter: BiFilter, + ) : BiConsumer { + var results: HashSet = HashSet() + + override fun accept( + k: K, + v: V, + ) { + if (filter.filter(k, v)) { + results.add(v) + } + } + } + + fun interface BiMapper { + fun map( + k: K, + v: V, + ): R? + } + + fun interface BiMapperNotNull { + fun map( + k: K, + v: V, + ): R + } + + class BiMapCollector( + val mapper: BiMapper, + ) : BiConsumer { + var results: ArrayList = ArrayList() + + override fun accept( + k: K, + v: V, + ) { + val result = mapper.map(k, v) + if (result != null) { + results.add(result) + } + } + } + + class BiAssociateCollector( + val size: Int, + val mapper: BiMapperNotNull>, + ) : BiConsumer { + var results: LinkedHashMap = LinkedHashMap(size) + + override fun accept( + k: K, + v: V, + ) { + val pair = mapper.map(k, v) + results.put(pair.first, pair.second) + } + } + + class BiAssociateNotNullCollector( + val size: Int, + val mapper: BiMapper?>, + ) : BiConsumer { + var results: LinkedHashMap = LinkedHashMap(size) + + override fun accept( + k: K, + v: V, + ) { + val pair = mapper.map(k, v) + if (pair != null) { + results.put(pair.first, pair.second) + } + } + } + + class BiAssociateWithCollector( + val size: Int, + val mapper: BiMapper, + ) : BiConsumer { + var results: LinkedHashMap = LinkedHashMap(size) + + override fun accept( + k: K, + v: V, + ) { + results.put(k, mapper.map(k, v)) + } + } + + class BiAssociateNotNullWithCollector( + val size: Int, + val mapper: BiMapper, + ) : BiConsumer { + var results: LinkedHashMap = LinkedHashMap(size) + + override fun accept( + k: K, + v: V, + ) { + val newValue = mapper.map(k, v) + if (newValue != null) { + results.put(k, newValue) + } + } + } + + class BiMapUniqueCollector( + val mapper: BiMapper, + ) : BiConsumer { + var results: HashSet = HashSet() + + override fun accept( + k: K, + v: V, + ) { + val result = mapper.map(k, v) + if (result != null) { + results.add(result) + } + } + } + + class BiMapFlattenCollector( + val mapper: BiMapper?>, + ) : BiConsumer { + var results: ArrayList = ArrayList() + + override fun accept( + k: K, + v: V, + ) { + val result = mapper.map(k, v) + if (result != null) { + results.addAll(result) + } + } + } + + class BiMapFlattenUniqueCollector( + val mapper: BiMapper?>, + ) : BiConsumer { + var results: HashSet = HashSet() + + override fun accept( + k: K, + v: V, + ) { + val result = mapper.map(k, v) + if (result != null) { + results.addAll(result) + } + } + } + + fun interface BiNotNullMapper { + fun map( + k: K, + v: V, + ): R + } + + class BiNotNullMapCollector( + val mapper: BiNotNullMapper, + ) : BiConsumer { + var results: ArrayList = ArrayList() + + override fun accept( + k: K, + v: V, + ) { + results.add(mapper.map(k, v)) + } + } + + fun interface BiSumOf { + fun map( + k: K, + v: V, + ): Int + } + + class BiMaxOfCollector( + val filter: BiFilter, + val comparator: Comparator, + ) : BiConsumer { + var maxK: K? = null + var maxV: V? = null + + override fun accept( + k: K, + v: V, + ) { + if (filter.filter(k, v)) { + if (maxK == null || comparator.compare(v, maxV) > 0) { + maxK = k + maxV = v + } + } + } + } + + class BiSumOfCollector( + val mapper: BiSumOf, + ) : BiConsumer { + var sum = 0 + + override fun accept( + k: K, + v: V, + ) { + sum += mapper.map(k, v) + } + } + + fun interface BiSumOfLong { + fun map( + k: K, + v: V, + ): Long + } + + class BiSumOfLongCollector( + val mapper: BiSumOfLong, + ) : BiConsumer { + var sum = 0L + + override fun accept( + k: K, + v: V, + ) { + sum += mapper.map(k, v) + } + } + + class BiGroupByCollector( + val mapper: BiNotNullMapper, + ) : BiConsumer { + var results = HashMap>() + + override fun accept( + k: K, + v: V, + ) { + val group = mapper.map(k, v) + + val list = results[group] + if (list == null) { + val answer = ArrayList() + answer.add(v) + results[group] = answer + } else { + list.add(v) + } + } + } + + class BiCountByGroupCollector( + val mapper: BiNotNullMapper, + ) : BiConsumer { + var results = HashMap() + + override fun accept( + k: K, + v: V, + ) { + val group = mapper.map(k, v) + + val count = results[group] + if (count == null) { + results[group] = 1 + } else { + results[group] = count + 1 + } + } + } + + class BiSumByGroupCollector( + val mapper: BiNotNullMapper, + val sumOf: BiNotNullMapper, + ) : BiConsumer { + var results = HashMap() + + override fun accept( + k: K, + v: V, + ) { + val group = mapper.map(k, v) + + val sum = results[group] + if (sum == null) { + results[group] = sumOf.map(k, v) + } else { + results[group] = sum + sumOf.map(k, v) + } + } + } + + class BiCountIfCollector( + val filter: BiFilter, + ) : BiConsumer { + var count = 0 + + override fun accept( + k: K, + v: V, + ) { + if (filter.filter(k, v)) count++ + } + } +}