package com.rssuper.services import com.rssuper.parsing.FeedParser import com.rssuper.parsing.ParseResult import okhttp3.Call import okhttp3.EventListener import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response import java.io.IOException import java.util.concurrent.TimeUnit class FeedFetcher( private val timeoutMs: Long = 15000, private val maxRetries: Int = 3, private val baseRetryDelayMs: Long = 1000 ) { private val client: OkHttpClient init { val builder = OkHttpClient.Builder() .connectTimeout(timeoutMs, TimeUnit.MILLISECONDS) .readTimeout(timeoutMs, TimeUnit.MILLISECONDS) .writeTimeout(timeoutMs, TimeUnit.MILLISECONDS) builder.eventListenerFactory { call -> TimeoutEventListener(call) } client = builder.build() } fun fetch( url: String, httpAuth: HTTPAuthCredentials? = null, ifNoneMatch: String? = null, ifModifiedSince: String? = null ): NetworkResult { var lastError: Throwable? = null for (attempt in 1..maxRetries) { val result = fetchSingleAttempt(url, httpAuth, ifNoneMatch, ifModifiedSince) when (result) { is NetworkResult.Success -> return result is NetworkResult.Failure -> { lastError = result.error if (attempt < maxRetries) { val delay = calculateBackoffDelay(attempt) Thread.sleep(delay) } } } } return NetworkResult.Failure(lastError ?: NetworkError.Unknown()) } fun fetchAndParse(url: String, httpAuth: HTTPAuthCredentials? = null): NetworkResult { val fetchResult = fetch(url, httpAuth) return fetchResult.flatMap { result -> try { val parseResult = FeedParser.parse(result.feedXml, url) NetworkResult.Success(parseResult) } catch (e: Exception) { NetworkResult.Failure(NetworkError.Unknown(e)) } } } private fun fetchSingleAttempt( url: String, httpAuth: HTTPAuthCredentials? = null, ifNoneMatch: String? = null, ifModifiedSince: String? = null ): NetworkResult { val requestBuilder = Request.Builder() .url(url) .addHeader("User-Agent", "RSSuper/1.0") ifNoneMatch?.let { requestBuilder.addHeader("If-None-Match", it) } ifModifiedSince?.let { requestBuilder.addHeader("If-Modified-Since", it) } httpAuth?.let { requestBuilder.addHeader("Authorization", it.toCredentials()) } val request = requestBuilder.build() return try { val response = client.newCall(request).execute() handleResponse(response, url) } catch (e: IOException) { NetworkResult.Failure(NetworkError.Unknown(e)) } catch (e: Exception) { NetworkResult.Failure(NetworkError.Unknown(e)) } } private fun handleResponse(response: Response, url: String): NetworkResult { try { val body = response.body return when (response.code) { 200 -> { if (body != null) { NetworkResult.Success(FetchResult.fromResponse(response, url, response.cacheResponse != null)) } else { NetworkResult.Failure(NetworkError.Http(response.code, "Empty response body")) } } 304 -> { if (body != null) { NetworkResult.Success(FetchResult.fromResponse(response, url, true)) } else { NetworkResult.Failure(NetworkError.Http(response.code, "Empty response body")) } } in 400..499 -> { NetworkResult.Failure(NetworkError.Http(response.code, "Client error: ${response.message}")) } in 500..599 -> { NetworkResult.Failure(NetworkError.Http(response.code, "Server error: ${response.message}")) } else -> { NetworkResult.Failure(NetworkError.Http(response.code, "Unexpected status code: ${response.code}")) } } } finally { response.close() } } private fun calculateBackoffDelay(attempt: Int): Long { var delay = baseRetryDelayMs for (i in 1 until attempt) { delay *= 2 } return delay } private class TimeoutEventListener(private val call: Call) : EventListener() { override fun callStart(call: Call) { } override fun callEnd(call: Call) { } override fun callFailed(call: Call, ioe: IOException) { } } sealed class NetworkResult { data class Success(val value: T) : NetworkResult() data class Failure(val error: Throwable) : NetworkResult() fun isSuccess(): Boolean = this is Success fun isFailure(): Boolean = this is Failure fun getOrNull(): T? = when (this) { is Success -> value is Failure -> null } fun map(transform: (T) -> R): NetworkResult = when (this) { is Success -> Success(transform(value)) is Failure -> Failure(error) } fun flatMap(transform: (T) -> NetworkResult): NetworkResult = when (this) { is Success -> transform(value) is Failure -> Failure(error) } } }