Skip to content

Commit 547c928

Browse files
authored
Merge pull request #5 from frankois944/4-implement-missing-cachestorage-methods
Add new deletion api
2 parents 3c59db7 + bd987bc commit 547c928

File tree

9 files changed

+3064
-319
lines changed

9 files changed

+3064
-319
lines changed

KtorKMPFileCaching/build.gradle.kts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ kotlin {
4343
}
4444
@OptIn(ExperimentalWasmDsl::class)
4545
wasmJs {
46-
moduleName = "ktorfilecaching"
46+
outputModuleName = "ktorfilecaching"
4747
browser {
4848
testTask {
4949
useKarma {
@@ -127,15 +127,14 @@ kotlin {
127127
implementation(libs.ktor.client.mock)
128128
implementation(libs.kotlin.coroutines.test)
129129
implementation(libs.ktor.client.logging)
130-
implementation(libs.kotlinx.datetime)
131130
implementation(libs.kotlin.serialization)
132131
}
133132
}
134133
}
135134

136135
android {
137136
namespace = "fr.frankois944.ktorfilecaching"
138-
compileSdk = 34
137+
compileSdk = 36
139138
defaultConfig {
140139
minSdk = 24
141140
}
@@ -150,7 +149,7 @@ mavenPublishing {
150149
coordinates(
151150
groupId = "io.github.frankois944",
152151
artifactId = "ktorfilecaching",
153-
version = "0.7.0",
152+
version = "0.8.0",
154153
)
155154

156155
// Configure POM metadata for the published artifact
@@ -185,7 +184,7 @@ mavenPublishing {
185184
}
186185

187186
// Configure publishing to Maven Central
188-
publishToMavenCentral(com.vanniktech.maven.publish.SonatypeHost.CENTRAL_PORTAL)
187+
publishToMavenCentral()
189188

190189
// Enable GPG signing for all publications
191190
signAllPublications()

KtorKMPFileCaching/src/browserStorageSystemMain/kotlin/fr/frankois944/ktorfilecaching/KtorFileCaching.localStorageSystemMain.kt

Lines changed: 70 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,29 @@ import io.ktor.client.plugins.cache.storage.CacheStorage
66
import io.ktor.client.plugins.cache.storage.CachedResponseData
77
import io.ktor.http.Url
88
import io.ktor.util.collections.ConcurrentMap
9+
import io.ktor.util.logging.KtorSimpleLogger
10+
import io.ktor.util.logging.trace
911
import kotlinx.coroutines.CoroutineDispatcher
1012
import kotlinx.coroutines.coroutineScope
13+
import kotlinx.coroutines.launch
1114
import kotlinx.coroutines.sync.Mutex
1215
import kotlinx.coroutines.sync.withLock
1316
import kotlinx.coroutines.withContext
1417
import kotlinx.serialization.ExperimentalSerializationApi
1518
import kotlinx.serialization.cbor.Cbor
19+
import kotlinx.serialization.decodeFromByteArray
1620
import kotlinx.serialization.decodeFromHexString
21+
import kotlinx.serialization.encodeToByteArray
1722
import kotlinx.serialization.encodeToHexString
1823
import okio.ByteString.Companion.encodeUtf8
1924
import okio.FileSystem
2025
import okio.HashingSink
2126
import okio.Path
27+
import okio.Path.Companion.toPath
2228
import okio.blackholeSink
2329
import okio.buffer
2430
import okio.use
31+
import kotlin.collections.emptySet
2532

2633
internal val prefix = "fr.frankois944.ktorfilecaching_key"
2734

@@ -41,14 +48,18 @@ internal class FileCacheStorage(
4148
) : CacheStorage {
4249
private val mutexes = ConcurrentMap<String, Mutex>()
4350

51+
@Suppress("ktlint:standard:property-naming")
52+
private val LOGGER = KtorSimpleLogger("KtorFileCaching")
53+
4454
override suspend fun store(
4555
url: Url,
4656
data: CachedResponseData,
4757
): Unit =
4858
withContext(dispatcher) {
4959
val urlHex = key(url)
50-
val caches = readCache(urlHex).filterNot { it.varyKeys == data.varyKeys } + data
51-
writeCache(urlHex, caches)
60+
updateCache(urlHex) { caches ->
61+
caches.filterNot { it.varyKeys == data.varyKeys } + data
62+
}
5263
}
5364

5465
override suspend fun findAll(url: Url): Set<CachedResponseData> = readCache(key(url))
@@ -58,7 +69,24 @@ internal class FileCacheStorage(
5869
varyKeys: Map<String, String>,
5970
): CachedResponseData? {
6071
val data = readCache(key(url))
61-
return data.find { varyKeys.all { (key, value) -> it.varyKeys[key] == value } }
72+
return data.find {
73+
varyKeys.all { (key, value) -> it.varyKeys[key] == value }
74+
}
75+
}
76+
77+
override suspend fun remove(
78+
url: Url,
79+
varyKeys: Map<String, String>,
80+
) {
81+
val urlHex = key(url)
82+
updateCache(urlHex) { caches ->
83+
caches.filterNot { it.varyKeys == varyKeys }
84+
}
85+
}
86+
87+
override suspend fun removeAll(url: Url) {
88+
val urlHex = key(url)
89+
deleteCache(urlHex)
6290
}
6391

6492
private fun key(url: Url): String {
@@ -69,27 +97,53 @@ internal class FileCacheStorage(
6997
return hashingSink.hash.hex()
7098
}
7199

72-
private suspend fun writeCache(
100+
private suspend fun readCache(urlHex: String): Set<CachedResponseData> {
101+
val mutex = mutexes.computeIfAbsent(urlHex) { Mutex() }
102+
return mutex.withLock { readCacheUnsafe(urlHex) }
103+
}
104+
105+
private suspend inline fun updateCache(
73106
urlHex: String,
74-
caches: List<CachedResponseData>,
75-
) = coroutineScope {
107+
transform: (Set<CachedResponseData>) -> List<CachedResponseData>,
108+
) {
76109
val mutex = mutexes.computeIfAbsent(urlHex) { Mutex() }
77-
mutex.withLock {
78-
val serializedData = Cbor.encodeToHexString(caches.map { SerializableCachedResponseData(it) })
79-
Database.setItem("${prefix}_$urlHex", serializedData)
110+
return mutex.withLock {
111+
val caches = readCacheUnsafe(urlHex)
112+
writeCacheUnsafe(urlHex, transform(caches))
80113
}
81114
}
82115

83-
private suspend fun readCache(urlHex: String): Set<CachedResponseData> {
116+
private suspend fun deleteCache(urlHex: String) {
84117
val mutex = mutexes.computeIfAbsent(urlHex) { Mutex() }
85-
return mutex.withLock {
86-
val item = Database.getItem("${prefix}_$urlHex")
87-
if (item == null) return@withLock emptySet()
118+
mutex.withLock {
88119
try {
89-
Cbor.decodeFromHexString<Set<SerializableCachedResponseData>>(item).map { it.cachedResponseData }.toSet()
90-
} catch (e: Exception) {
91-
emptySet()
120+
if (Database.getItem("${prefix}_$urlHex") == null) return@withLock
121+
Database.removeItem("${prefix}_$urlHex")
122+
} catch (cause: Exception) {
123+
LOGGER.trace { "Exception during cache deletion in a file: ${cause.stackTraceToString()}" }
92124
}
93125
}
94126
}
127+
128+
private suspend fun writeCacheUnsafe(
129+
urlHex: String,
130+
caches: List<CachedResponseData>,
131+
) = coroutineScope {
132+
try {
133+
val serializedData = Cbor.encodeToHexString(caches.map { SerializableCachedResponseData(it) })
134+
Database.setItem("${prefix}_$urlHex", serializedData)
135+
} catch (cause: Exception) {
136+
LOGGER.trace { "Exception during saving a cache to a file: ${cause.stackTraceToString()}" }
137+
}
138+
}
139+
140+
private suspend fun readCacheUnsafe(urlHex: String): Set<CachedResponseData> {
141+
return try {
142+
val item = Database.getItem("${prefix}_$urlHex") ?: return emptySet()
143+
Cbor.decodeFromHexString<Set<SerializableCachedResponseData>>(item).map { it.cachedResponseData }.toSet()
144+
} catch (cause: Exception) {
145+
LOGGER.trace { "Exception during cache lookup in a file: ${cause.stackTraceToString()}" }
146+
emptySet()
147+
}
148+
}
95149
}

KtorKMPFileCaching/src/commonMain/kotlin/fr/frankois944/ktorfilecaching/CachingCacheStorage.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,17 @@ internal class CachingCacheStorage(
3838
}
3939
return store.getValue(url)
4040
}
41+
42+
override suspend fun remove(
43+
url: Url,
44+
varyKeys: Map<String, String>,
45+
) {
46+
delegate.remove(url, varyKeys)
47+
store[url] = delegate.findAll(url)
48+
}
49+
50+
override suspend fun removeAll(url: Url) {
51+
delegate.removeAll(url)
52+
store.remove(url)
53+
}
4154
}

0 commit comments

Comments
 (0)