git.iko.soy/opencode-android
on e66c7ed help sign in
opencode-android/app/src/main/java/soy/iko/opencode/di/OpencodeConnection.kt
52 lines 2.5 kB history raw
1package soy.iko.opencode.di
3import soy.iko.opencode.data.model.ServerProfile
4import soy.iko.opencode.data.network.EventStreamClient
5import soy.iko.opencode.data.network.HttpClientFactory
6import soy.iko.opencode.data.network.OpencodeApiClient
7import soy.iko.opencode.data.repo.MessageCacheStore
8import soy.iko.opencode.data.repo.SessionRepository
9import kotlinx.coroutines.CoroutineScope
10import kotlinx.coroutines.Dispatchers
11import kotlinx.coroutines.SupervisorJob
12import kotlinx.coroutines.cancel
13import soy.iko.opencode.util.runCatchingCancellable
15/**
16 * An active connection to one opencode server. Bundles the Ktor client, REST API,
17 * SSE event stream, and the reducing repository, all sharing a connection-scoped
18 * coroutine scope that is cancelled on [close].
19 */
20open class OpencodeConnection(
21 val profile: ServerProfile,
22 // Optional on-disk message cache, supplied by AppContainer, so conversations render
23 // instantly and stay readable offline. Null in the test constructor.
24 private val messageCache: MessageCacheStore? = null,
25) {
27 protected constructor() : this(ServerProfile(id = "", label = "", baseUrl = "http://localhost"), null)
29 private val scopeJob = SupervisorJob()
30 private val scope by lazy { CoroutineScope(scopeJob + Dispatchers.IO) }
31 private val client by lazy { HttpClientFactory.create(profile) }
33 open val api: OpencodeApiClient by lazy { OpencodeApiClient(client) }
34 open val events: EventStreamClient by lazy { EventStreamClient(client, scope) }
35 open val repository: SessionRepository by lazy { SessionRepository(api, events, messageCache, profile.id) }
37 /**
38 * Cancel the coroutine scope and wait for in-flight work (including the SSE reader)
39 * to wind down before closing the HTTP client. Calling client.close() while the SSE
40 * coroutine is still mid-request produces a spurious "stream error, will retry"
41 * warning during normal teardown.
42 */
43 open suspend fun close() {
44 scope.cancel()
45 scopeJob.join()
46 // runCatchingCancellable (not plain runCatching) per the repo convention: close() is a
47 // suspend function, and plain runCatching swallows CancellationException — if the
48 // caller's scope is cancelled mid-close, the cancellation must propagate rather than
49 // be eaten as a Result.failure (which would leave the client half-closed).
50 runCatchingCancellable { client.close() }
51 }