fix(auth): await cookie scope hydration on cold start (#961) (#962)

* fix(auth): await cookie scope hydration before ticket mint and request loading (#961)

* refactor(auth): scope latch activation to active store hydration

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
M57andM57 authored and GitHub committed 2026-08-23 23:57:37 +03:00
1 parent 913ae1275d
commit 764cb3455f
3 files changed
+66 -9

No files matched your search

@@ -11,6 +11,7 @@ import okhttp3.CookieJar
import okhttp3.HttpUrl
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicReference
/**
@@ -63,6 +64,9 @@ class PersistentCookieJar(
*/
fun getCookie(name: String): Cookie? {
val serverId = currentServerId.get()
if (!loadedScopes.contains(serverId)) {
loadLatches[serverId]?.await(5, TimeUnit.SECONDS)
}
val hosts = cache[serverId] ?: return null
for (bucket in hosts.values) {
synchronized(bucket) {
@@ -126,6 +130,9 @@ class PersistentCookieJar(
override fun loadForRequest(url: HttpUrl): List<Cookie> {
val serverId = currentServerId.get()
if (!loadedScopes.contains(serverId)) {
loadLatches[serverId]?.await(5, TimeUnit.SECONDS)
}
val hostKey = url.host
val hosts = cache[serverId] ?: return emptyList()
val now = System.currentTimeMillis()
@@ -152,16 +159,21 @@ class PersistentCookieJar(
private suspend fun ensureLoaded(serverId: String) {
if (loadedScopes.contains(serverId)) return
val persisted = store.load(serverId)
val byHost = ConcurrentHashMap<String, MutableList<Cookie>>()
for (cookie in persisted) {
// Blank domain => host-only (e.g. legacy migrated session cookie).
// Bucket it under a wildcard "*" so it is returned for every host.
val host = cookie.domain.removePrefix(".").ifBlank { WILDCARD_HOST }
byHost.getOrPut(host) { mutableListOf() }.add(cookie)
val latch = loadLatches.computeIfAbsent(serverId) { CountDownLatch(1) }
try {
val persisted = store.load(serverId)
val byHost = ConcurrentHashMap<String, MutableList<Cookie>>()
for (cookie in persisted) {
// Blank domain => host-only (e.g. legacy migrated session cookie).
// Bucket it under a wildcard "*" so it is returned for every host.
val host = cookie.domain.removePrefix(".").ifBlank { WILDCARD_HOST }
byHost.getOrPut(host) { mutableListOf() }.add(cookie)
}
cache[serverId] = byHost
loadedScopes.add(serverId)
} finally {
latch.countDown()
}
cache[serverId] = byHost
loadedScopes.add(serverId)
}
private fun persist(serverId: String) {
@@ -195,6 +207,7 @@ class PersistentCookieJar(
val scope = currentServerId.get()
cache.remove(scope)
loadedScopes.remove(scope)
loadLatches.remove(scope)
storeScope.launch { store.clear(scope) }
}
@@ -202,6 +215,7 @@ class PersistentCookieJar(
fun clearAll() {
cache.clear()
loadedScopes.clear()
loadLatches.clear()
storeScope.launch { store.clearAll() }
}
@@ -4,6 +4,7 @@ import android.util.Log
import androidx.annotation.VisibleForTesting
import com.m57.hermescontrol.BuildConfig
import com.m57.hermescontrol.data.local.AuthManager
import com.m57.hermescontrol.data.remote.CookieManager
import com.m57.hermescontrol.data.remote.DashboardSessionTokenRefresher
import com.m57.hermescontrol.data.remote.NetworkMonitor
import com.m57.hermescontrol.data.remote.OkHttpProvider
@@ -493,6 +494,9 @@ object HermesWsClient {
// read throws NetworkOnMainThreadException and the mint fails.
val (code, body) =
kotlinx.coroutines.runBlocking(Dispatchers.IO) {
if (CookieManager.isInitialized()) {
CookieManager.useStore(CookieManager.cookieJar.currentServer())
}
client.newCall(request).execute().use { resp ->
resp.code to resp.body.string()
}