Skip to content

Commit 44822c2

Browse files
committed
Replace mutex-based caching with suspendLazy for font bytes and codepoints, decode in Dispatchers.Default
1 parent 7d0a042 commit 44822c2

5 files changed

Lines changed: 125 additions & 133 deletions

File tree

tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/bootstrap/data/BootstrapRepository.kt

Lines changed: 16 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
package io.github.composegears.valkyrie.ui.screen.webimport.standard.bootstrap.data
22

3+
import io.github.composegears.valkyrie.util.coroutines.suspendLazy
34
import io.github.composegears.valkyrie.util.font.Woff2Decoder
45
import io.ktor.client.HttpClient
56
import io.ktor.client.request.get
67
import io.ktor.client.statement.bodyAsChannel
78
import io.ktor.client.statement.bodyAsText
89
import io.ktor.utils.io.toByteArray
910
import kotlinx.coroutines.Dispatchers
10-
import kotlinx.coroutines.sync.Mutex
11-
import kotlinx.coroutines.sync.withLock
1211
import kotlinx.coroutines.withContext
1312
import kotlinx.serialization.json.Json
1413

@@ -23,34 +22,28 @@ class BootstrapRepository(
2322
private const val ICONS_BASE_URL = "$UNPKG_BASE/icons"
2423
}
2524

26-
private val fontMutex = Mutex()
27-
private val codepointMutex = Mutex()
28-
private var fontBytesCache: ByteArray? = null
29-
private var codepointsCache: Map<String, Int>? = null
30-
31-
suspend fun loadFontBytes(): ByteArray = withContext(Dispatchers.IO) {
32-
fontMutex.withLock {
33-
fontBytesCache ?: run {
34-
val woff2Bytes = httpClient.get(FONT_URL).bodyAsChannel().toByteArray()
35-
val ttfBytes = Woff2Decoder.decodeBytes(woff2Bytes)
36-
?: error("Failed to decode WOFF2 font")
37-
fontBytesCache = ttfBytes
38-
ttfBytes
25+
private val fontBytes = suspendLazy {
26+
withContext(Dispatchers.IO) {
27+
val woff2Bytes = httpClient.get(FONT_URL).bodyAsChannel().toByteArray()
28+
29+
withContext(Dispatchers.Default) {
30+
Woff2Decoder.decodeBytes(woff2Bytes) ?: error("Failed to decode WOFF2 font")
3931
}
4032
}
4133
}
4234

43-
suspend fun loadCodepoints(): Map<String, Int> = withContext(Dispatchers.IO) {
44-
codepointMutex.withLock {
45-
codepointsCache ?: run {
46-
val jsonText = httpClient.get(JSON_URL).bodyAsText()
47-
val codepoints = json.decodeFromString<Map<String, Int>>(jsonText)
48-
codepointsCache = codepoints
49-
codepoints
50-
}
35+
private val codepoints = suspendLazy {
36+
withContext(Dispatchers.IO) {
37+
val jsonText = httpClient.get(JSON_URL).bodyAsText()
38+
39+
json.decodeFromString<Map<String, Int>>(jsonText)
5140
}
5241
}
5342

43+
suspend fun loadFontBytes(): ByteArray = fontBytes()
44+
45+
suspend fun loadCodepoints(): Map<String, Int> = codepoints()
46+
5447
suspend fun downloadSvg(iconName: String): String = withContext(Dispatchers.IO) {
5548
httpClient.get("$ICONS_BASE_URL/$iconName.svg").bodyAsText()
5649
}

tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/boxicons/data/BoxIconsRepository.kt

Lines changed: 14 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
package io.github.composegears.valkyrie.ui.screen.webimport.standard.boxicons.data
22

3+
import io.github.composegears.valkyrie.util.coroutines.suspendLazy
34
import io.github.composegears.valkyrie.util.font.Woff2Decoder
45
import io.ktor.client.HttpClient
56
import io.ktor.client.request.get
67
import io.ktor.client.statement.bodyAsChannel
78
import io.ktor.client.statement.bodyAsText
89
import io.ktor.utils.io.toByteArray
910
import kotlinx.coroutines.Dispatchers
10-
import kotlinx.coroutines.sync.Mutex
11-
import kotlinx.coroutines.sync.withLock
1211
import kotlinx.coroutines.withContext
1312

1413
class BoxIconsRepository(
@@ -20,34 +19,27 @@ class BoxIconsRepository(
2019
private const val FONT_WOFF2_URL = "$UNPKG_BASE/fonts/boxicons.woff2"
2120
}
2221

23-
private val codepointsMutex = Mutex()
24-
private val fontMutex = Mutex()
25-
private var codepointsCache: List<BoxIconsCodepoint>? = null
26-
private var fontBytesCache: ByteArray? = null
27-
28-
suspend fun loadCodepoints(): List<BoxIconsCodepoint> = withContext(Dispatchers.IO) {
29-
codepointsMutex.withLock {
30-
codepointsCache ?: run {
31-
val cssText = httpClient.get(CSS_URL).bodyAsText()
32-
parseBoxIconsCodepoints(cssText).also { codepointsCache = it }
33-
}
22+
private val codepoints = suspendLazy {
23+
withContext(Dispatchers.IO) {
24+
val cssText = httpClient.get(CSS_URL).bodyAsText()
25+
parseBoxIconsCodepoints(cssText)
3426
}
3527
}
3628

37-
suspend fun loadFontBytes(): ByteArray = withContext(Dispatchers.IO) {
38-
fontMutex.withLock {
39-
fontBytesCache ?: run {
40-
val decodedFont = runCatching {
41-
val woff2Bytes = httpClient.get(FONT_WOFF2_URL).bodyAsChannel().toByteArray()
42-
Woff2Decoder.decodeBytes(woff2Bytes)
43-
}.getOrNull() ?: error("Failed to decode BoxIcons WOFF2 font")
29+
private val fontBytes = suspendLazy {
30+
withContext(Dispatchers.IO) {
31+
val woff2Bytes = httpClient.get(FONT_WOFF2_URL).bodyAsChannel().toByteArray()
4432

45-
fontBytesCache = decodedFont
46-
decodedFont
33+
withContext(Dispatchers.Default) {
34+
Woff2Decoder.decodeBytes(woff2Bytes) ?: error("Failed to decode BoxIcons WOFF2 font")
4735
}
4836
}
4937
}
5038

39+
suspend fun loadCodepoints(): List<BoxIconsCodepoint> = codepoints()
40+
41+
suspend fun loadFontBytes(): ByteArray = fontBytes()
42+
5143
suspend fun downloadSvg(iconName: String): String = withContext(Dispatchers.IO) {
5244
val stylePath = when {
5345
iconName.startsWith("bxs-") -> "solid"

tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/lucide/data/LucideRepository.kt

Lines changed: 21 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
package io.github.composegears.valkyrie.ui.screen.webimport.standard.lucide.data
22

3+
import io.github.composegears.valkyrie.util.coroutines.suspendLazy
34
import io.github.composegears.valkyrie.util.font.Woff2Decoder
45
import io.ktor.client.HttpClient
56
import io.ktor.client.request.get
67
import io.ktor.client.statement.bodyAsChannel
78
import io.ktor.client.statement.bodyAsText
89
import io.ktor.utils.io.toByteArray
910
import kotlinx.coroutines.Dispatchers
10-
import kotlinx.coroutines.sync.Mutex
11-
import kotlinx.coroutines.sync.withLock
1211
import kotlinx.coroutines.withContext
1312
import kotlinx.serialization.json.Json
1413
import kotlinx.serialization.json.JsonObject
@@ -25,10 +24,26 @@ class LucideRepository(
2524
private const val CSS_URL = "https://cdn.jsdelivr.net/npm/lucide-static@latest/font/lucide.css"
2625
}
2726

28-
private val fontMutex = Mutex()
29-
private val codepointMutex = Mutex()
30-
private var fontBytesCache: ByteArray? = null
31-
private var codepointsCache: Map<String, Int>? = null
27+
private val fontBytes = suspendLazy {
28+
withContext(Dispatchers.IO) {
29+
val woff2Bytes = httpClient.get(FONT_URL).bodyAsChannel().toByteArray()
30+
31+
withContext(Dispatchers.Default) {
32+
Woff2Decoder.decodeBytes(woff2Bytes) ?: error("Failed to decode WOFF2 font")
33+
}
34+
}
35+
}
36+
37+
private val codepoints = suspendLazy {
38+
withContext(Dispatchers.IO) {
39+
val cssText = httpClient.get(CSS_URL).bodyAsText()
40+
parseCodepoints(cssText)
41+
}
42+
}
43+
44+
suspend fun loadFontBytes(): ByteArray = fontBytes()
45+
46+
suspend fun loadCodepoints(): Map<String, Int> = codepoints()
3247

3348
suspend fun loadIconList(): List<Pair<String, LucideIconMetadata>> = withContext(Dispatchers.IO) {
3449
val response = httpClient.get("$UNPKG_BASE/tags.json")
@@ -43,28 +58,6 @@ class LucideRepository(
4358
}
4459
}
4560

46-
suspend fun loadFontBytes(): ByteArray = withContext(Dispatchers.IO) {
47-
fontMutex.withLock {
48-
fontBytesCache ?: run {
49-
val woff2Bytes = httpClient.get(FONT_URL).bodyAsChannel().toByteArray()
50-
val ttfBytes = Woff2Decoder.decodeBytes(woff2Bytes) ?: error("Failed to decode WOFF2 font")
51-
fontBytesCache = ttfBytes
52-
ttfBytes
53-
}
54-
}
55-
}
56-
57-
suspend fun loadCodepoints(): Map<String, Int> = withContext(Dispatchers.IO) {
58-
codepointMutex.withLock {
59-
codepointsCache ?: run {
60-
val cssText = httpClient.get(CSS_URL).bodyAsText()
61-
val codepoints = parseCodepoints(cssText)
62-
codepointsCache = codepoints
63-
codepoints
64-
}
65-
}
66-
}
67-
6861
suspend fun downloadSvg(iconName: String): String = withContext(Dispatchers.IO) {
6962
httpClient.get("$UNPKG_BASE/icons/$iconName.svg").bodyAsText()
7063
}

tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/remix/data/RemixRepository.kt

Lines changed: 42 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
package io.github.composegears.valkyrie.ui.screen.webimport.standard.remix.data
22

3+
import io.github.composegears.valkyrie.util.coroutines.suspendLazy
34
import io.github.composegears.valkyrie.util.font.Woff2Decoder
45
import io.ktor.client.HttpClient
56
import io.ktor.client.request.get
67
import io.ktor.client.statement.bodyAsChannel
78
import io.ktor.client.statement.bodyAsText
89
import io.ktor.utils.io.toByteArray
910
import kotlinx.coroutines.Dispatchers
10-
import kotlinx.coroutines.sync.Mutex
11-
import kotlinx.coroutines.sync.withLock
1211
import kotlinx.coroutines.withContext
1312
import kotlinx.serialization.json.Json
1413
import kotlinx.serialization.json.jsonArray
@@ -28,15 +27,45 @@ class RemixRepository(
2827
private val CODEPOINT_REGEX = Regex("""\.ri-([a-z0-9-]+)::?before\s*\{\s*content:\s*["']\\([a-fA-F0-9]+)["'];?\s*}""")
2928
}
3029

31-
private val fontMutex = Mutex()
32-
private val codepointMutex = Mutex()
33-
private val metadataMutex = Mutex()
34-
private val versionMutex = Mutex()
30+
private val fontBytes = suspendLazy {
31+
withContext(Dispatchers.IO) {
32+
val woff2Bytes = httpClient.get(FONT_URL).bodyAsChannel().toByteArray()
33+
withContext(Dispatchers.Default) {
34+
Woff2Decoder.decodeBytes(woff2Bytes) ?: error("Failed to decode WOFF2 font")
35+
}
36+
}
37+
}
3538

36-
private var fontBytesCache: ByteArray? = null
37-
private var codepointsCache: Map<String, Int>? = null
38-
private var svgMetadataCache: RemixSvgMetadata? = null
39-
private var remixVersionCache: String? = null
39+
private val codepoints = suspendLazy {
40+
withContext(Dispatchers.IO) {
41+
val cssText = httpClient.get(CSS_URL).bodyAsText()
42+
parseCodepoints(cssText)
43+
}
44+
}
45+
46+
private val svgMetadata = suspendLazy {
47+
withContext(Dispatchers.IO) {
48+
val flatIndexJson = httpClient.get(loadFlatIndexUrl()).bodyAsText()
49+
val (svgPathByName, categoryByName) = parseSvgMetadata(flatIndexJson)
50+
RemixSvgMetadata(
51+
svgPathByName = svgPathByName,
52+
categoryByName = categoryByName,
53+
)
54+
}
55+
}
56+
57+
suspend fun loadFontBytes(): ByteArray = fontBytes()
58+
59+
private val remixVersion = suspendLazy {
60+
withContext(Dispatchers.IO) {
61+
val packageJson = httpClient.get(PACKAGE_JSON_URL).bodyAsText()
62+
json.parseToJsonElement(packageJson)
63+
.jsonObject["version"]
64+
?.jsonPrimitive
65+
?.content
66+
?: error("Failed to resolve Remix package version")
67+
}
68+
}
4069

4170
suspend fun loadIconList(): Map<String, RemixIconMetadata> {
4271
val codepoints = loadCodepoints()
@@ -51,70 +80,23 @@ class RemixRepository(
5180
}
5281
}
5382

54-
suspend fun loadFontBytes(): ByteArray = withContext(Dispatchers.IO) {
55-
fontMutex.withLock {
56-
fontBytesCache ?: run {
57-
val woff2Bytes = httpClient.get(FONT_URL).bodyAsChannel().toByteArray()
58-
val ttfBytes = Woff2Decoder.decodeBytes(woff2Bytes)
59-
?: error("Failed to decode WOFF2 font")
60-
fontBytesCache = ttfBytes
61-
ttfBytes
62-
}
63-
}
64-
}
65-
6683
suspend fun downloadSvg(iconName: String): String = withContext(Dispatchers.IO) {
6784
val iconPath = loadSvgMetadata().svgPathByName[iconName]
6885
?: error("SVG path not found for Remix icon: $iconName")
6986

7087
httpClient.get("$CDN_BASE/$iconPath").bodyAsText()
7188
}
7289

73-
private suspend fun loadCodepoints(): Map<String, Int> = withContext(Dispatchers.IO) {
74-
codepointMutex.withLock {
75-
codepointsCache ?: run {
76-
val cssText = httpClient.get(CSS_URL).bodyAsText()
77-
val codepoints = parseCodepoints(cssText)
78-
codepointsCache = codepoints
79-
codepoints
80-
}
81-
}
82-
}
90+
private suspend fun loadCodepoints(): Map<String, Int> = codepoints()
8391

84-
private suspend fun loadSvgMetadata(): RemixSvgMetadata = withContext(Dispatchers.IO) {
85-
metadataMutex.withLock {
86-
svgMetadataCache ?: run {
87-
val flatIndexJson = httpClient.get(loadFlatIndexUrl()).bodyAsText()
88-
val (svgPathByName, categoryByName) = parseSvgMetadata(flatIndexJson)
89-
RemixSvgMetadata(
90-
svgPathByName = svgPathByName,
91-
categoryByName = categoryByName,
92-
).also { metadata ->
93-
svgMetadataCache = metadata
94-
}
95-
}
96-
}
97-
}
92+
private suspend fun loadSvgMetadata(): RemixSvgMetadata = svgMetadata()
9893

9994
private suspend fun loadFlatIndexUrl(): String {
10095
val version = loadPackageVersion()
10196
return FLAT_INDEX_URL_TEMPLATE.format(version)
10297
}
10398

104-
private suspend fun loadPackageVersion(): String = withContext(Dispatchers.IO) {
105-
versionMutex.withLock {
106-
remixVersionCache ?: run {
107-
val packageJson = httpClient.get(PACKAGE_JSON_URL).bodyAsText()
108-
val version = json.parseToJsonElement(packageJson)
109-
.jsonObject["version"]
110-
?.jsonPrimitive
111-
?.content
112-
?: error("Failed to resolve Remix package version")
113-
remixVersionCache = version
114-
version
115-
}
116-
}
117-
}
99+
private suspend fun loadPackageVersion(): String = remixVersion()
118100

119101
private fun parseCodepoints(cssText: String): Map<String, Int> = CODEPOINT_REGEX
120102
.findAll(cssText)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package io.github.composegears.valkyrie.util.coroutines
2+
3+
import kotlinx.coroutines.sync.Mutex
4+
import kotlinx.coroutines.sync.withLock
5+
6+
class SuspendLazyProperty<T>(
7+
private val initializer: suspend () -> T,
8+
) {
9+
private val mutex = Mutex()
10+
11+
@Volatile
12+
private var value: Any? = Uninitialized
13+
14+
suspend operator fun invoke(): T = getValue()
15+
16+
@Suppress("UNCHECKED_CAST")
17+
private suspend fun getValue(): T {
18+
val v1 = value
19+
if (v1 !== Uninitialized) return v1 as T
20+
21+
return mutex.withLock {
22+
val v2 = value
23+
if (v2 !== Uninitialized) return v2 as T
24+
25+
initializer().also { value = it }
26+
}
27+
}
28+
29+
private object Uninitialized
30+
}
31+
32+
fun <T> suspendLazy(initializer: suspend () -> T): SuspendLazyProperty<T> = SuspendLazyProperty(initializer)

0 commit comments

Comments
 (0)