From 51f005d22f09f6baed975c9cb3caf87b299453e9 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Jul 2026 21:56:12 +0000 Subject: [PATCH] refactor(commons): unify the rich-text parser cache across Android + Desktop Replaces the two forked cached parsers (amethyst CachedRichTextParser on android.util.LruCache + desktop DesktopCachedRichTextParser on ConcurrentLruCache with a naive isMarkdown) with one shared object in commons/jvmAndroid/richtext, built on quartz ConcurrentLruCache and keeping amethyst's CommonMark-aware computeIsMarkdown and content-addressed key (content+tags+callbackUri+authorPubKey). - Add ConcurrentLruCache.trimToSize(maxItems) (+ tests) for the onTrimMemory path. - Repoint all amethyst callers (incl. the markdown unit test) and both desktop callers; delete both forks. Verified: :commons JVM, :desktopApp, :amethyst play debug + unit tests compile. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01LUiGxXMbjVmgspa1X15o1V --- .../com/vitorpamplona/amethyst/AppModules.kt | 2 +- .../ui/components/AmethystRichText.kt | 2 +- .../amethyst/ui/components/RichTextViewer.kt | 2 +- .../amethyst/ui/feeds/PrefetchFeedMedia.kt | 2 +- .../amethyst/ui/note/MultiSetCompose.kt | 2 +- .../ui/note/creators/previews/PreviewState.kt | 2 +- .../note/elements/DisplayUncitedHashtags.kt | 2 +- .../CachedRichTextParserMarkdownTest.kt | 2 +- .../commons/richtext}/CachedRichTextParser.kt | 44 ++++++++------- .../service/DesktopCachedRichTextParser.kt | 54 ------------------- .../desktop/ui/note/DesktopRichText.kt | 4 +- .../amethyst/desktop/ui/note/NoteCard.kt | 4 +- .../quartz/utils/cache/ConcurrentLruCache.kt | 14 +++++ .../utils/cache/ConcurrentLruCacheTest.kt | 32 +++++++++++ 14 files changed, 83 insertions(+), 85 deletions(-) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/service => commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext}/CachedRichTextParser.kt (89%) delete mode 100644 desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/DesktopCachedRichTextParser.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt index 393e9b64d8..f61737d7bd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt @@ -28,6 +28,7 @@ import coil3.disk.DiskCache import coil3.memory.MemoryCache import com.vitorpamplona.amethyst.commons.model.NoteState import com.vitorpamplona.amethyst.commons.relayClient.BlockedRelayFilteringClient +import com.vitorpamplona.amethyst.commons.richtext.CachedRichTextParser import com.vitorpamplona.amethyst.commons.robohash.CachedRobohash import com.vitorpamplona.amethyst.commons.scheduledposts.ScheduledPostStore import com.vitorpamplona.amethyst.commons.service.lnurl.OkHttpLnurlEndpointResolver @@ -51,7 +52,6 @@ import com.vitorpamplona.amethyst.model.torState.AccountsTorStateConnector import com.vitorpamplona.amethyst.model.torState.TorRelayState import com.vitorpamplona.amethyst.napplet.DataStoreNappletPermissionStore import com.vitorpamplona.amethyst.napplet.DataStoreNostrSignerPermissionStore -import com.vitorpamplona.amethyst.service.CachedRichTextParser import com.vitorpamplona.amethyst.service.calendar.CalendarReminderPrefs import com.vitorpamplona.amethyst.service.calendar.CalendarReminderWorker import com.vitorpamplona.amethyst.service.cast.CastRegistry diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/AmethystRichText.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/AmethystRichText.kt index 82ba3d55b3..0bab9aad6d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/AmethystRichText.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/AmethystRichText.kt @@ -35,6 +35,7 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalUriHandler import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists import com.vitorpamplona.amethyst.commons.richtext.BlossomUriSegment +import com.vitorpamplona.amethyst.commons.richtext.CachedRichTextParser import com.vitorpamplona.amethyst.commons.richtext.CashuSegment import com.vitorpamplona.amethyst.commons.richtext.ClinkOfferSegment import com.vitorpamplona.amethyst.commons.richtext.ConcordInviteLinkSegment @@ -54,7 +55,6 @@ import com.vitorpamplona.amethyst.commons.ui.richtext.LocalRichTextInteractions import com.vitorpamplona.amethyst.commons.ui.richtext.LocalRichTextSegmentRenderer import com.vitorpamplona.amethyst.commons.ui.richtext.RichTextInteractions import com.vitorpamplona.amethyst.commons.ui.richtext.RichTextSegmentRenderer -import com.vitorpamplona.amethyst.service.CachedRichTextParser import com.vitorpamplona.amethyst.ui.components.markdown.RenderContentAsMarkdown import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt index 5bede8afe0..176b1aab3a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt @@ -69,6 +69,7 @@ import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists import com.vitorpamplona.amethyst.commons.richtext.Base64Segment import com.vitorpamplona.amethyst.commons.richtext.BechSegment import com.vitorpamplona.amethyst.commons.richtext.BlossomUriSegment +import com.vitorpamplona.amethyst.commons.richtext.CachedRichTextParser import com.vitorpamplona.amethyst.commons.richtext.CashuSegment import com.vitorpamplona.amethyst.commons.richtext.ClinkOfferSegment import com.vitorpamplona.amethyst.commons.richtext.ConcordInviteLinkSegment @@ -102,7 +103,6 @@ import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.checkForHashtagWithIcon -import com.vitorpamplona.amethyst.service.CachedRichTextParser import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderFilterAssemblerSubscription import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserNickname diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/PrefetchFeedMedia.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/PrefetchFeedMedia.kt index 27d1601cf0..abebeaaa13 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/PrefetchFeedMedia.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/PrefetchFeedMedia.kt @@ -35,6 +35,7 @@ import coil3.imageLoader import coil3.request.ImageRequest import coil3.request.SuccessResult import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists +import com.vitorpamplona.amethyst.commons.richtext.CachedRichTextParser import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo import com.vitorpamplona.amethyst.commons.richtext.RichTextParser @@ -44,7 +45,6 @@ import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState import com.vitorpamplona.amethyst.model.MediaAspectRatioCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.UrlCachedPreviewer -import com.vitorpamplona.amethyst.service.CachedRichTextParser import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt index 36ef874ded..c880e1ecc4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt @@ -69,11 +69,11 @@ import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder import com.vitorpamplona.amethyst.commons.hashtags.Cashu import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons import com.vitorpamplona.amethyst.commons.model.EmptyTagList +import com.vitorpamplona.amethyst.commons.richtext.CachedRichTextParser import com.vitorpamplona.amethyst.commons.ui.components.AnimatedBorderTextCornerRadius import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.CachedRichTextParser import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderFilterAssemblerSubscription import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserContactCardsScore import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserPicture diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/previews/PreviewState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/previews/PreviewState.kt index 09adafb3f7..ad97ee17c2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/previews/PreviewState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/previews/PreviewState.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.ui.note.creators.previews import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.service.CachedUrlParser +import com.vitorpamplona.amethyst.commons.richtext.CachedUrlParser import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.MutableStateFlow diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayUncitedHashtags.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayUncitedHashtags.kt index 48e7fa591d..fa2e816825 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayUncitedHashtags.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayUncitedHashtags.kt @@ -27,9 +27,9 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.produceState import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists +import com.vitorpamplona.amethyst.commons.richtext.CachedRichTextParser import com.vitorpamplona.amethyst.commons.richtext.HashTagSegment import com.vitorpamplona.amethyst.commons.ui.components.ClickableTextColor -import com.vitorpamplona.amethyst.service.CachedRichTextParser import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/CachedRichTextParserMarkdownTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/CachedRichTextParserMarkdownTest.kt index 366e7e322e..731a31cc5a 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/CachedRichTextParserMarkdownTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/CachedRichTextParserMarkdownTest.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst -import com.vitorpamplona.amethyst.service.CachedRichTextParser +import com.vitorpamplona.amethyst.commons.richtext.CachedRichTextParser import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/CachedRichTextParser.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/CachedRichTextParser.kt similarity index 89% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/CachedRichTextParser.kt rename to commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/CachedRichTextParser.kt index 5287ce9eb5..6f4b18c3fe 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/CachedRichTextParser.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/CachedRichTextParser.kt @@ -18,21 +18,27 @@ * 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.service +package com.vitorpamplona.amethyst.commons.richtext -import android.util.LruCache import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists -import com.vitorpamplona.amethyst.commons.richtext.RichTextParser -import com.vitorpamplona.amethyst.commons.richtext.RichTextViewerState -import com.vitorpamplona.amethyst.commons.richtext.UrlParser +import com.vitorpamplona.quartz.utils.cache.ConcurrentLruCache +/** + * The shared, cross-platform cache in front of [RichTextParser]. Both Amethyst + * Android and Amethyst Desktop render the same parsed [RichTextViewerState] from + * one place, so the same content quoted in multiple notes is only parsed once. + * + * Lives in `jvmAndroid` because it depends on [ConcurrentLruCache] (a JCA-free, + * lock-free-read LRU that is not in `commonMain`). iOS/`commonMain` callers use + * the uncached [RichTextParser] directly until a KMP cache is available. + */ object CachedRichTextParser { // Global across every feed. Sized to hold the active feed's visible + prefetched - // (see PrefetchFeedMedia) working set plus a few other feeds' recent entries, so - // pre-parsed bodies survive until the render reads them and feed switches don't - // thrash. Each entry is one note's parsed segments — typically single-digit KB. - private val richTextCache = LruCache(500) - private val isMarkdownCache = LruCache(200) + // working set plus a few other feeds' recent entries, so pre-parsed bodies survive + // until the render reads them and feed switches don't thrash. Each entry is one + // note's parsed segments — typically single-digit KB. + private val richTextCache = ConcurrentLruCache(500) + private val isMarkdownCache = ConcurrentLruCache(200) private fun hashCodeCache( content: String, @@ -69,7 +75,7 @@ object CachedRichTextParser { tags: ImmutableListOfLists, callbackUri: String? = null, authorPubKey: String? = null, - ): RichTextViewerState? = richTextCache[hashCodeCache(content, tags, callbackUri, authorPubKey)] + ): RichTextViewerState? = richTextCache.get(hashCodeCache(content, tags, callbackUri, authorPubKey)) fun parseText( content: String, @@ -78,13 +84,13 @@ object CachedRichTextParser { authorPubKey: String? = null, ): RichTextViewerState { val key = hashCodeCache(content, tags, callbackUri, authorPubKey) - val cached = richTextCache[key] + val cached = richTextCache.get(key) return if (cached != null) { cached } else { - val newUrls = RichTextParser().parseText(content, tags, callbackUri, authorPubKey) - richTextCache.put(key, newUrls) - newUrls + val newState = RichTextParser().parseText(content, tags, callbackUri, authorPubKey) + richTextCache.put(key, newState) + newState } } @@ -92,7 +98,7 @@ object CachedRichTextParser { // notes only pays for the scan once. The decision is purely a function of `content`. fun isMarkdown(content: String): Boolean { val key = content.hashCode() - isMarkdownCache[key]?.let { return it } + isMarkdownCache.get(key)?.let { return it } val result = computeIsMarkdown(content) isMarkdownCache.put(key, result) return result @@ -296,15 +302,15 @@ object CachedRichTextParser { } object CachedUrlParser { - private val parsedUrlsCache = LruCache>(10) + private val parsedUrlsCache = ConcurrentLruCache>(10) - fun cachedParseValidUrls(content: String): List = parsedUrlsCache[content.hashCode()] + fun cachedParseValidUrls(content: String): List? = parsedUrlsCache.get(content.hashCode()) fun parseValidUrls(content: String): List { if (content.isEmpty()) return emptyList() val key = content.hashCode() - val cached = parsedUrlsCache[key] + val cached = parsedUrlsCache.get(key) return if (cached != null) { cached } else { diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/DesktopCachedRichTextParser.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/DesktopCachedRichTextParser.kt deleted file mode 100644 index 0d17dd3db0..0000000000 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/DesktopCachedRichTextParser.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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.desktop.service - -import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists -import com.vitorpamplona.amethyst.commons.richtext.RichTextParser -import com.vitorpamplona.amethyst.commons.richtext.RichTextViewerState -import com.vitorpamplona.quartz.utils.cache.ConcurrentLruCache - -object DesktopCachedRichTextParser { - private const val MAX_CACHE_SIZE = 50 - - // Lock-free get on the feed rich-text render path; the previous access-order - // synchronizedMap took a monitor even on reads. - private val cache = ConcurrentLruCache(MAX_CACHE_SIZE) - - fun parseText( - content: String, - tags: ImmutableListOfLists, - callbackUri: String? = null, - ): RichTextViewerState { - cache.get(content)?.let { return it } - val state = RichTextParser().parseText(content, tags, callbackUri) - cache.put(content, state) - return state - } - - fun isMarkdown(content: String): Boolean = - content.startsWith("> ") || - content.startsWith("# ") || - content.contains("##") || - content.contains("__") || - content.contains("**") || - content.contains("```") || - content.contains("](") -} diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/note/DesktopRichText.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/note/DesktopRichText.kt index 1484abd00f..6804aeea43 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/note/DesktopRichText.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/note/DesktopRichText.kt @@ -57,6 +57,7 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.richtext.Base64Segment import com.vitorpamplona.amethyst.commons.richtext.BechSegment +import com.vitorpamplona.amethyst.commons.richtext.CachedRichTextParser import com.vitorpamplona.amethyst.commons.richtext.CashuSegment import com.vitorpamplona.amethyst.commons.richtext.ImageGalleryParagraph import com.vitorpamplona.amethyst.commons.richtext.ImageSegment @@ -75,7 +76,6 @@ import com.vitorpamplona.amethyst.commons.ui.richtext.LocalRichTextSegmentRender import com.vitorpamplona.amethyst.commons.ui.richtext.RichTextInteractions import com.vitorpamplona.amethyst.commons.ui.richtext.RichTextSegmentRenderer import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache -import com.vitorpamplona.amethyst.desktop.service.DesktopCachedRichTextParser import com.vitorpamplona.quartz.lightning.LnInvoiceUtil import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent @@ -112,7 +112,7 @@ fun DesktopRichText( callbacks: RichTextCallbacks = RichTextCallbacks(), modifier: Modifier = Modifier, ) { - if (DesktopCachedRichTextParser.isMarkdown(content)) { + if (CachedRichTextParser.isMarkdown(content)) { RenderMarkdown( content = content, onLinkClick = { url -> handleDesktopLinkClick(url, callbacks) }, diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/note/NoteCard.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/note/NoteCard.kt index c425feeb1d..adf0b732ca 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/note/NoteCard.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/note/NoteCard.kt @@ -56,12 +56,12 @@ import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.commons.model.EmptyTagList import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists +import com.vitorpamplona.amethyst.commons.richtext.CachedRichTextParser import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.commons.richtext.UrlParser import com.vitorpamplona.amethyst.commons.ui.note.ReplyContext import com.vitorpamplona.amethyst.commons.ui.note.ReplyToLabel import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache -import com.vitorpamplona.amethyst.desktop.service.DesktopCachedRichTextParser import com.vitorpamplona.amethyst.desktop.ui.components.ToggleableTimeAgoText import com.vitorpamplona.amethyst.desktop.ui.media.AnimatedGifImage import com.vitorpamplona.amethyst.desktop.ui.media.AudioPlayer @@ -293,7 +293,7 @@ fun NoteCard( if (strippedContent.isNotBlank()) { val richState = remember(strippedContent, note.tags) { - DesktopCachedRichTextParser.parseText(strippedContent, note.tags) + CachedRichTextParser.parseText(strippedContent, note.tags) } DesktopRichText( content = strippedContent, diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/cache/ConcurrentLruCache.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/cache/ConcurrentLruCache.kt index 94896edde8..eff79f0f92 100644 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/cache/ConcurrentLruCache.kt +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/cache/ConcurrentLruCache.kt @@ -87,5 +87,19 @@ class ConcurrentLruCache( } } + /** + * Evicts the oldest entries until at most [maxItems] remain. Used to shed + * memory on pressure (e.g. `onTrimMemory`). A [maxItems] of 0 or less clears + * the cache; values above the current size are a no-op. + */ + fun trimToSize(maxItems: Int) { + synchronized(writeLock) { + while (order.size > maxItems) { + val oldest = order.removeFirstOrNull() ?: break + map.remove(oldest) + } + } + } + fun size(): Int = map.size } diff --git a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/utils/cache/ConcurrentLruCacheTest.kt b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/utils/cache/ConcurrentLruCacheTest.kt index 23a0ee7a66..1baa537dbf 100644 --- a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/utils/cache/ConcurrentLruCacheTest.kt +++ b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/utils/cache/ConcurrentLruCacheTest.kt @@ -100,6 +100,38 @@ class ConcurrentLruCacheTest { assertNull(cache.get("a")) } + @Test + fun `trimToSize evicts oldest down to the target`() { + val cache = ConcurrentLruCache(10) + cache.put("a", 1) + cache.put("b", 2) + cache.put("c", 3) + cache.trimToSize(1) + assertEquals(1, cache.size()) + // Oldest (a, b) evicted; newest (c) kept. + assertNull(cache.get("a")) + assertNull(cache.get("b")) + assertEquals(3, cache.get("c")) + } + + @Test + fun `trimToSize above current size is a no-op`() { + val cache = ConcurrentLruCache(10) + cache.put("a", 1) + cache.put("b", 2) + cache.trimToSize(5) + assertEquals(2, cache.size()) + } + + @Test + fun `trimToSize to zero clears`() { + val cache = ConcurrentLruCache(10) + cache.put("a", 1) + cache.trimToSize(0) + assertEquals(0, cache.size()) + assertNull(cache.get("a")) + } + @Test fun `size never exceeds capacity under concurrent puts`() { val cap = 100