Bluesky feed server - NSFW Likes

feat: Cache identity responses

Changed files
+54 -3
darkfeed
gradle
+1
darkfeed/build.gradle.kts
··· 21 21 implementation(libs.ktor.serialization.kotlinx.json) 22 22 implementation(libs.java.jwt) 23 23 implementation(libs.clikt) 24 + implementation(libs.kache) 24 25 } 25 26 26 27 java {
+3 -1
darkfeed/src/main/kotlin/Main.kt
··· 93 93 } catch (error: Exception) { 94 94 log.warn("Unknown failure occurred: {}", error.message) 95 95 null 96 - } finally { 96 + } 97 + 98 + if (authAccount == null) { 97 99 log.warn("Authorized requests (e.g. updating feed generator record) will not be available") 98 100 } 99 101
+46 -2
darkfeed/src/main/kotlin/api/AtpIdentityClient.kt
··· 1 1 package rs.averyrive.darkfeed.api 2 2 3 + import com.mayakapps.kache.InMemoryKache 3 4 import io.ktor.client.* 4 5 import io.ktor.client.call.* 5 6 import io.ktor.client.engine.cio.* ··· 14 15 import kotlinx.serialization.json.Json 15 16 import org.slf4j.LoggerFactory 16 17 import rs.averyrive.darkfeed.api.model.* 18 + import kotlin.time.Duration.Companion.hours 19 + 20 + // Specify size of each cache 21 + const val CACHE_SIZE: Long = 1 * 1024 * 1024 // 1 MB 17 22 18 23 /** Client that handles identity related requests for ATProto accounts. */ 19 24 class AtpIdentityClient { ··· 32 37 install(Logging) 33 38 } 34 39 40 + /** Cache of handle to DID relationships. */ 41 + private val handleDidCache = InMemoryKache<String, Did>(CACHE_SIZE) { 42 + expireAfterWriteDuration = 1.hours 43 + } 44 + 45 + /** Cache of DID to PDS host relationships. */ 46 + private val didPdsCache = InMemoryKache<Did, String>(CACHE_SIZE) { 47 + expireAfterWriteDuration = 1.hours 48 + } 49 + 35 50 /** 36 51 * Resolve a handle's DID. 37 52 * ··· 46 61 */ 47 62 suspend fun resolveDidFromHandle(handle: String): Did { 48 63 log.debug("Resolving DID for '{}'...", handle) 64 + 65 + // Return cached DID if it exists. 66 + handleDidCache.get(handle)?.let { did -> 67 + log.debug("Returning cached DID for '{}': {}", handle, did.toString()) 68 + return did 69 + } 49 70 50 71 @Serializable 51 72 data class ResolveHandleResponse(val did: String) ··· 101 122 else -> throw RuntimeException("Received unexpected response: ${resolveHandleResponse.bodyAsText()}") 102 123 } 103 124 104 - log.debug("Resolved DID for '{}': {}", handle, did.toString()) 125 + // Cache the DID for this handle. 126 + handleDidCache.put(handle, did) 127 + log.debug("Resolved and cached DID for '{}': {}", handle, did.toString()) 105 128 106 129 return did 107 130 } ··· 120 143 */ 121 144 suspend fun resolvePdsFromDid(did: Did): String { 122 145 log.debug("Resolving PDS for '{}'...", did.toString()) 146 + 147 + // Return cached PDS host if it exists. 148 + didPdsCache.get(did)?.let { pdsHost -> 149 + log.debug("Returning cached PDS host for '{}': {}", did.toString(), pdsHost) 150 + return pdsHost 151 + } 123 152 124 153 // Get the DID document, using either `plc.directory` or 125 154 // `/.well-known/did.json` depending on the type of DID. ··· 166 195 ?.serviceEndpoint 167 196 ?: throw PdsNotFoundException(did) 168 197 169 - log.debug("Resolved PDS for '{}': {}", did.toString(), pdsHost) 198 + // Cache the PDS for this DID. 199 + didPdsCache.put(did, pdsHost) 200 + log.debug("Resolved and cached PDS for '{}': {}", did.toString(), pdsHost) 170 201 171 202 return pdsHost 203 + } 204 + 205 + /** 206 + * Invalidate the PDS associated with the given DID. 207 + * 208 + * This should be used when a call to the PDS fails due to the PDS no longer 209 + * being available. This may indicate that the PDS has moved and should be 210 + * resolved again. 211 + * 212 + * @param did DID to invalidate. 213 + */ 214 + suspend fun invalidateCachedPds(did: Did) { 215 + didPdsCache.remove(did) 172 216 } 173 217 } 174 218
+2
darkfeed/src/main/kotlin/api/BskyApi.kt
··· 92 92 val record: Generator, 93 93 ) 94 94 95 + // TODO: If this PDS is not accessible, invalidate cache and retry. 95 96 val ownerPdsHost = atpIdentityClient.resolvePdsFromDid(authManager.authAccountDid.toDid()) 96 97 97 98 val response = httpClient.post { ··· 116 117 @Serializable 117 118 data class Response(val cursor: String?, val records: List<LikeRef>) 118 119 120 + // TODO: If this PDS is not accessible, invalidate cache and retry. 119 121 val requestorPdsHost = atpIdentityClient.resolvePdsFromDid(actor.toDid()) 120 122 121 123 val response = httpClient.get {
+2
gradle/libs.versions.toml
··· 4 4 ktor = "3.0.1" 5 5 auth0 = "4.4.0" 6 6 clikt = "5.0.1" 7 + kache = "2.1.1" 7 8 8 9 [libraries] 9 10 logback-classic = { group = "ch.qos.logback", name = "logback-classic", version.ref = "logback" } ··· 18 19 ktor-serialization-kotlinx-json = { group = "io.ktor", name = "ktor-serialization-kotlinx-json", version.ref = "ktor" } 19 20 java-jwt = { group = "com.auth0", name = "java-jwt", version.ref = "auth0" } 20 21 clikt = { group = "com.github.ajalt.clikt", name = "clikt", version.ref = "clikt" } 22 + kache = { group = "com.mayakapps.kache", name = "kache", version.ref = "kache" } 21 23 22 24 [plugins] 23 25 kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }