Adds submap queries to the addressable large cache

This commit is contained in:
Vitor Pamplona
2025-10-06 18:04:37 -04:00
parent b546294078
commit 5f59f1ab41
14 changed files with 422 additions and 12 deletions

View File

@@ -26,7 +26,7 @@ import java.util.concurrent.ConcurrentSkipListMap
import java.util.function.BiConsumer
class LargeSoftCache<K, V> : CacheOperations<K, V> {
private val cache = ConcurrentSkipListMap<K, WeakReference<V>>()
protected val cache = ConcurrentSkipListMap<K, WeakReference<V>>()
fun keys() = cache.keys
@@ -125,6 +125,14 @@ class LargeSoftCache<K, V> : CacheOperations<K, V> {
cache.forEach(BiConsumerWrapper(this, consumer))
}
override fun forEach(
from: K,
to: K,
consumer: BiConsumer<K, V>,
) {
cache.subMap(from, to).forEach(BiConsumerWrapper(this, consumer))
}
class BiConsumerWrapper<K, V>(
val cache: LargeSoftCache<K, V>,
val inner: BiConsumer<K, V>,

View File

@@ -0,0 +1,74 @@
/**
* 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.amethyst.model
import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.utils.cache.CacheCollectors
const val START_KEY = "0000000000000000000000000000000000000000000000000000000000000000"
fun kindStart(
kind: Int,
pubKey: HexKey,
) = Address(kind, pubKey, "")
fun kindEnd(
kind: Int,
pubKey: HexKey,
) = Address(kind + 1, pubKey, "")
fun kindStart(kind: Int) = kindStart(kind, START_KEY)
fun kindEnd(kind: Int) = kindEnd(kind + 1, START_KEY)
fun LargeSoftCache<Address, AddressableNote>.filterIntoSet(
kind: Int,
consumer: CacheCollectors.BiFilter<Address, AddressableNote>,
): Set<AddressableNote> = filterIntoSet(kindStart(kind), kindEnd(kind), consumer)
fun LargeSoftCache<Address, AddressableNote>.filterIntoSet(
kinds: List<Int>,
consumer: CacheCollectors.BiFilter<Address, AddressableNote>,
): Set<AddressableNote> {
val set = mutableSetOf<AddressableNote>()
kinds.forEach {
set.addAll(filterIntoSet(kindStart(it), kindEnd(it), consumer))
}
return set
}
fun LargeSoftCache<Address, AddressableNote>.filterIntoSet(
kind: Int,
pubKey: HexKey,
consumer: CacheCollectors.BiFilter<Address, AddressableNote>,
): Set<AddressableNote> = filterIntoSet(kindStart(kind, pubKey), kindEnd(kind, pubKey), consumer)
fun <R> LargeSoftCache<Address, AddressableNote>.mapNotNullIntoSet(
kind: Int,
consumer: CacheCollectors.BiMapper<Address, AddressableNote, R?>,
): Set<R> = mapNotNullIntoSet(kindStart(kind), kindEnd(kind), consumer)
fun <R> LargeSoftCache<Address, AddressableNote>.mapNotNullIntoSet(
kind: Int,
pubKey: HexKey,
consumer: CacheCollectors.BiMapper<Address, AddressableNote, R?>,
): Set<R> = mapNotNullIntoSet(kindStart(kind, pubKey), kindEnd(kind, pubKey), consumer)

View File

@@ -22,7 +22,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.LocalCache.notes
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.filterIntoSet
import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
import com.vitorpamplona.amethyst.ui.dal.FilterByListParams
@@ -45,13 +47,11 @@ open class DiscoverLongFormFeedFilter(
override fun feed(): List<Note> {
val params = buildFilterParams(account)
val notes =
LocalCache.addressables.filterIntoSet { _, it ->
LocalCache.addressables.filterIntoSet(LongTextNoteEvent.KIND) { _, it ->
val noteEvent = it.event
noteEvent is LongTextNoteEvent && params.match(noteEvent)
}
return sort(notes)
}

View File

@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.filterIntoSet
import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
import com.vitorpamplona.amethyst.ui.dal.FilterByListParams
@@ -45,7 +46,7 @@ open class DiscoverFollowSetsFeedFilter(
val params = buildFilterParams(account)
val notes =
LocalCache.addressables.filterIntoSet { _, it ->
LocalCache.addressables.filterIntoSet(FollowListEvent.KIND) { _, it ->
val noteEvent = it.event
noteEvent is FollowListEvent && params.match(noteEvent)
}

View File

@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.mapNotNullIntoSet
import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
import com.vitorpamplona.amethyst.ui.dal.FilterByListParams
import com.vitorpamplona.quartz.nip01Core.core.Address
@@ -54,7 +55,7 @@ open class DiscoverCommunityFeedFilter(
// Here we only need to look for CommunityDefinition Events
val notes =
LocalCache.addressables.mapNotNullIntoSet { key, note ->
LocalCache.addressables.mapNotNullIntoSet(CommunityDefinitionEvent.KIND) { key, note ->
val noteEvent = note.event
if (noteEvent == null && shouldInclude(key, filterParams, note.relays)) {
// send unloaded communities to the screen

View File

@@ -24,6 +24,7 @@ import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.ParticipantListBuilder
import com.vitorpamplona.amethyst.model.filterIntoSet
import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavFilter
import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByProxyTopNavFilter
import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavFilter
@@ -55,7 +56,7 @@ open class DiscoverNIP89FeedFilter(
override fun feed(): List<Note> {
val notes =
LocalCache.addressables.filterIntoSet { _, it ->
LocalCache.addressables.filterIntoSet(AppDefinitionEvent.KIND) { _, it ->
acceptDVM(it)
}

View File

@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.filterIntoSet
import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
import com.vitorpamplona.amethyst.ui.dal.FilterByListParams
import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent
@@ -46,7 +47,7 @@ open class DiscoverMarketplaceFeedFilter(
val params = buildFilterParams(account)
val notes =
LocalCache.addressables.filterIntoSet { _, it ->
LocalCache.addressables.filterIntoSet(ClassifiedsEvent.KIND) { _, it ->
val noteEvent = it.event
noteEvent is ClassifiedsEvent && noteEvent.isWellFormed() && params.match(noteEvent)
}

View File

@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.drafts.dal
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.filterIntoSet
import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
@@ -39,7 +40,7 @@ class DraftEventsFeedFilter(
override fun feed(): List<Note> {
val drafts =
LocalCache.addressables.filterIntoSet { _, note ->
LocalCache.addressables.filterIntoSet(DraftWrapEvent.KIND, account.userProfile().pubkeyHex) { _, note ->
acceptableEvent(note)
}

View File

@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.dal
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.filterIntoSet
import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter
import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByProxyTopNavFilter
import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
@@ -45,6 +46,17 @@ import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent
class HomeNewThreadFeedFilter(
val account: Account,
) : AdditiveFeedFilter<Note>() {
companion object {
val ADDRESSABLE_KINDS =
listOf(
AudioTrackEvent.KIND,
InteractiveStoryPrologueEvent.KIND,
WikiNoteEvent.KIND,
ClassifiedsEvent.KIND,
LongTextNoteEvent.KIND,
)
}
override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + account.settings.defaultHomeFollowList.value
override fun showHiddenKey(): Boolean =
@@ -67,7 +79,9 @@ class HomeNewThreadFeedFilter(
}
val longFormNotes =
LocalCache.addressables.filterIntoSet { _, note ->
LocalCache.addressables.filterIntoSet(
kinds = ADDRESSABLE_KINDS,
) { _, note ->
acceptableEvent(note, filterParams)
}

View File

@@ -24,9 +24,11 @@ import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.filterIntoSet
import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
import com.vitorpamplona.amethyst.ui.dal.FilterByListParams
import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent
import com.vitorpamplona.quartz.experimental.forks.forkFromVersion
import com.vitorpamplona.quartz.experimental.forks.isForkFromAddressWithPubkey
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
@@ -37,6 +39,7 @@ import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
@@ -47,6 +50,11 @@ import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent
import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
import com.vitorpamplona.quartz.nip52Calendar.CalendarDateSlotEvent
import com.vitorpamplona.quartz.nip52Calendar.CalendarRSVPEvent
import com.vitorpamplona.quartz.nip52Calendar.CalendarTimeSlotEvent
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
import com.vitorpamplona.quartz.nip58Badges.BadgeDefinitionEvent
@@ -58,10 +66,25 @@ import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryRequestEvent
import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryResponseEvent
import com.vitorpamplona.quartz.nip90Dvms.NIP90StatusEvent
import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent
class NotificationFeedFilter(
val account: Account,
) : AdditiveFeedFilter<Note>() {
companion object {
val ADDRESSABLE_KINDS =
listOf(
AudioTrackEvent.KIND,
WikiNoteEvent.KIND,
ClassifiedsEvent.KIND,
LongTextNoteEvent.KIND,
CalendarTimeSlotEvent.KIND,
CalendarDateSlotEvent.KIND,
CalendarRSVPEvent.KIND,
LiveActivitiesEvent.KIND,
)
}
override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + account.settings.defaultNotificationFollowList.value
override fun showHiddenKey(): Boolean =
@@ -83,7 +106,7 @@ class NotificationFeedFilter(
LocalCache.notes.filterIntoSet { _, note ->
note.event !is AddressableEvent && acceptableEvent(note, filterParams)
} +
LocalCache.addressables.filterIntoSet { _, note ->
LocalCache.addressables.filterIntoSet(ADDRESSABLE_KINDS) { _, note ->
acceptableEvent(note, filterParams)
}

View File

@@ -24,7 +24,9 @@ import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.LocalCache.notes
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.filterIntoSet
import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
import com.vitorpamplona.amethyst.ui.dal.FilterByListParams
@@ -67,7 +69,10 @@ class VideoFeedFilter(
LocalCache.notes.filterIntoSet { _, it ->
acceptableEvent(it, params)
} +
LocalCache.addressables.filterIntoSet { _, it ->
LocalCache.addressables.filterIntoSet(VideoHorizontalEvent.KIND) { _, it ->
acceptableEvent(it, params)
} +
LocalCache.addressables.filterIntoSet(VideoVerticalEvent.KIND) { _, it ->
acceptableEvent(it, params)
}

View File

@@ -70,6 +70,108 @@ interface ICacheOperations<K, V> {
fun <U> associateWith(transform: (K, V) -> U?): Map<K, U?>
// --
// Sub map operations
// --
fun filter(
from: K,
to: K,
consumer: CacheCollectors.BiFilter<K, V>,
): List<V>
fun filterIntoSet(
from: K,
to: K,
consumer: CacheCollectors.BiFilter<K, V>,
): Set<V>
fun <R> map(
from: K,
to: K,
consumer: CacheCollectors.BiNotNullMapper<K, V, R>,
): List<R>
fun <R> mapNotNull(
from: K,
to: K,
consumer: CacheCollectors.BiMapper<K, V, R?>,
): List<R>
fun <R> mapNotNullIntoSet(
from: K,
to: K,
consumer: CacheCollectors.BiMapper<K, V, R?>,
): Set<R>
fun <R> mapFlatten(
from: K,
to: K,
consumer: CacheCollectors.BiMapper<K, V, Collection<R>?>,
): List<R>
fun <R> mapFlattenIntoSet(
from: K,
to: K,
consumer: CacheCollectors.BiMapper<K, V, Collection<R>?>,
): Set<R>
fun maxOrNullOf(
from: K,
to: K,
filter: CacheCollectors.BiFilter<K, V>,
comparator: Comparator<V>,
): V?
fun sumOf(
from: K,
to: K,
consumer: CacheCollectors.BiSumOf<K, V>,
): Int
fun sumOfLong(
from: K,
to: K,
consumer: CacheCollectors.BiSumOfLong<K, V>,
): Long
fun <R> groupBy(
from: K,
to: K,
consumer: CacheCollectors.BiNotNullMapper<K, V, R>,
): Map<R, List<V>>
fun <R> countByGroup(
from: K,
to: K,
consumer: CacheCollectors.BiNotNullMapper<K, V, R>,
): Map<R, Int>
fun <R> sumByGroup(
from: K,
to: K,
groupMap: CacheCollectors.BiNotNullMapper<K, V, R>,
sumOf: CacheCollectors.BiNotNullMapper<K, V, Long>,
): Map<R, Long>
fun count(
from: K,
to: K,
consumer: CacheCollectors.BiFilter<K, V>,
): Int
fun <T, U> associate(
from: K,
to: K,
transform: (K, V) -> Pair<T, U>,
): Map<T, U>
fun <U> associateWith(
from: K,
to: K,
transform: (K, V) -> U?,
): Map<K, U?>
fun joinToString(
separator: CharSequence = ", ",
prefix: CharSequence = "",

View File

@@ -36,6 +36,12 @@ class MyBiConsumer<K, V>(
interface CacheOperations<K, V> : ICacheOperations<K, V> {
fun forEach(consumer: BiConsumer<K, V>)
fun forEach(
from: K,
to: K,
consumer: BiConsumer<K, V>,
)
override fun size(): Int
override fun forEach(consumer: ICacheBiConsumer<K, V>) {
@@ -144,6 +150,171 @@ interface CacheOperations<K, V> : ICacheOperations<K, V> {
return runner.results
}
// ----
// submap operations
// ----
override fun filter(
from: K,
to: K,
consumer: CacheCollectors.BiFilter<K, V>,
): List<V> {
val runner = BiFilterCollector(consumer)
forEach(from, to, runner)
return runner.results
}
override fun filterIntoSet(
from: K,
to: K,
consumer: CacheCollectors.BiFilter<K, V>,
): Set<V> {
val runner = BiFilterUniqueCollector(consumer)
forEach(from, to, runner)
return runner.results
}
override fun <R> map(
from: K,
to: K,
consumer: CacheCollectors.BiNotNullMapper<K, V, R>,
): List<R> {
val runner = BiNotNullMapCollector(consumer)
forEach(from, to, runner)
return runner.results
}
override fun <R> mapNotNull(
from: K,
to: K,
consumer: CacheCollectors.BiMapper<K, V, R?>,
): List<R> {
val runner = BiMapCollector(consumer)
forEach(from, to, runner)
return runner.results
}
override fun <R> mapNotNullIntoSet(
from: K,
to: K,
consumer: CacheCollectors.BiMapper<K, V, R?>,
): Set<R> {
val runner = BiMapUniqueCollector(consumer)
forEach(from, to, runner)
return runner.results
}
override fun <R> mapFlatten(
from: K,
to: K,
consumer: CacheCollectors.BiMapper<K, V, Collection<R>?>,
): List<R> {
val runner = BiMapFlattenCollector(consumer)
forEach(from, to, runner)
return runner.results
}
override fun <R> mapFlattenIntoSet(
from: K,
to: K,
consumer: CacheCollectors.BiMapper<K, V, Collection<R>?>,
): Set<R> {
val runner = BiMapFlattenUniqueCollector(consumer)
forEach(from, to, runner)
return runner.results
}
override fun maxOrNullOf(
from: K,
to: K,
filter: CacheCollectors.BiFilter<K, V>,
comparator: Comparator<V>,
): V? {
val runner = BiMaxOfCollector(filter, comparator)
forEach(from, to, runner)
return runner.maxV
}
override fun sumOf(
from: K,
to: K,
consumer: CacheCollectors.BiSumOf<K, V>,
): Int {
val runner = BiSumOfCollector(consumer)
forEach(from, to, runner)
return runner.sum
}
override fun sumOfLong(
from: K,
to: K,
consumer: CacheCollectors.BiSumOfLong<K, V>,
): Long {
val runner = BiSumOfLongCollector(consumer)
forEach(from, to, runner)
return runner.sum
}
override fun <R> groupBy(
from: K,
to: K,
consumer: CacheCollectors.BiNotNullMapper<K, V, R>,
): Map<R, List<V>> {
val runner = BiGroupByCollector(consumer)
forEach(from, to, runner)
return runner.results
}
override fun <R> countByGroup(
from: K,
to: K,
consumer: CacheCollectors.BiNotNullMapper<K, V, R>,
): Map<R, Int> {
val runner = BiCountByGroupCollector(consumer)
forEach(from, to, runner)
return runner.results
}
override fun <R> sumByGroup(
from: K,
to: K,
groupMap: CacheCollectors.BiNotNullMapper<K, V, R>,
sumOf: CacheCollectors.BiNotNullMapper<K, V, Long>,
): Map<R, Long> {
val runner = BiSumByGroupCollector(groupMap, sumOf)
forEach(from, to, runner)
return runner.results
}
override fun count(
from: K,
to: K,
consumer: CacheCollectors.BiFilter<K, V>,
): Int {
val runner = BiCountIfCollector(consumer)
forEach(from, to, runner)
return runner.count
}
override fun <T, U> associate(
from: K,
to: K,
transform: (K, V) -> Pair<T, U>,
): Map<T, U> {
val runner = BiAssociateCollector(size(), transform)
forEach(from, to, runner)
return runner.results
}
override fun <U> associateWith(
from: K,
to: K,
transform: (K, V) -> U?,
): Map<K, U?> {
val runner = BiAssociateWithCollector(size(), transform)
forEach(from, to, runner)
return runner.results
}
override fun joinToString(
separator: CharSequence,
prefix: CharSequence,

View File

@@ -79,4 +79,12 @@ actual class LargeCache<K, V> : CacheOperations<K, V> {
override fun forEach(consumer: BiConsumer<K, V>) {
cache.forEach(consumer)
}
override fun forEach(
from: K,
to: K,
consumer: BiConsumer<K, V>,
) {
cache.subMap(from, to).forEach(consumer)
}
}