82f8bc62a0
- Add desktopApp module with JVM entry point and sidebar navigation - Add Claude specs for AI-assisted development: - Agent definitions: nostr-protocol, kotlin-multiplatform, compose-ui, kotlin-coroutines - Skills: quartz-kmp conversion, compose-desktop patterns - Commands: desktop-run, nip, extract - Update Gradle configuration with Compose Multiplatform 1.7.1 plugin - Add coroutines and secp256k1 JVM dependencies to version catalog Next steps: - Convert Quartz library to full KMP (expect/actual for crypto) - Implement relay connections in desktop app - Share UI components between Android and Desktop 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
6.8 KiB
6.8 KiB
Kotlin Coroutines Agent
Expertise Domain
This agent specializes in Kotlin coroutines and the kotlinx.coroutines library for asynchronous programming, reactive streams, and concurrent operations.
Core Knowledge Areas
Coroutine Fundamentals
// Suspending functions
suspend fun fetchNote(id: String): Note {
return withContext(Dispatchers.IO) {
api.getNote(id)
}
}
// Coroutine builders
fun main() = runBlocking {
// launch: fire-and-forget, returns Job
val job = launch {
delay(1000)
println("World")
}
// async: returns Deferred<T>
val deferred = async {
computeValue()
}
val result = deferred.await()
}
// Structured concurrency
suspend fun loadUserProfile(userId: String): UserProfile {
return coroutineScope {
val user = async { fetchUser(userId) }
val notes = async { fetchNotes(userId) }
val followers = async { fetchFollowers(userId) }
UserProfile(
user = user.await(),
notes = notes.await(),
followers = followers.await()
)
} // All complete or all cancel together
}
Dispatchers
| Dispatcher | Use Case | Notes |
|---|---|---|
Dispatchers.Main |
UI updates | Main thread (Android/Desktop) |
Dispatchers.IO |
Network, disk | Optimized for blocking I/O |
Dispatchers.Default |
CPU-intensive | Parallelism = CPU cores |
Dispatchers.Unconfined |
Testing only | Runs in caller's thread |
Flow (Cold Streams)
// Creating flows
fun observeNotes(): Flow<List<Note>> = flow {
while (true) {
val notes = repository.getNotes()
emit(notes)
delay(30_000) // Refresh every 30s
}
}
// Operators
repository.observeNotes()
.map { notes -> notes.filter { it.isVisible } }
.distinctUntilChanged()
.debounce(300)
.catch { e ->
log.error("Failed to load notes", e)
emit(emptyList())
}
.flowOn(Dispatchers.IO)
.collect { notes -> updateUI(notes) }
// Flow builders
val numbersFlow = flowOf(1, 2, 3, 4, 5)
val listFlow = listOf("a", "b", "c").asFlow()
StateFlow & SharedFlow (Hot Streams)
// StateFlow - always has a value, replays latest
class FeedViewModel {
private val _state = MutableStateFlow(FeedState())
val state: StateFlow<FeedState> = _state.asStateFlow()
fun updateFilter(filter: Filter) {
_state.update { current ->
current.copy(filter = filter)
}
}
}
// SharedFlow - no initial value, configurable replay
class EventBus {
private val _events = MutableSharedFlow<AppEvent>(
replay = 0,
extraBufferCapacity = 64,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
val events: SharedFlow<AppEvent> = _events.asSharedFlow()
suspend fun emit(event: AppEvent) {
_events.emit(event)
}
}
Channels
// Producer-consumer pattern
val channel = Channel<Event>(Channel.BUFFERED)
// Producer
launch {
for (event in eventSource) {
channel.send(event)
}
channel.close()
}
// Consumer
launch {
for (event in channel) {
process(event)
}
}
// channelFlow for complex producers
fun relayEvents(relay: Relay): Flow<Event> = channelFlow {
relay.connect()
relay.onEvent { event ->
trySend(event)
}
awaitClose { relay.disconnect() }
}
Cancellation & Exception Handling
// Cooperative cancellation
suspend fun processNotes(notes: List<Note>) {
for (note in notes) {
ensureActive() // Throws if cancelled
process(note)
yield() // Suspend point for cancellation
}
}
// Exception handling
val handler = CoroutineExceptionHandler { _, exception ->
log.error("Coroutine failed", exception)
}
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default + handler)
// supervisorScope: child failures don't cancel siblings
supervisorScope {
launch { task1() } // Can fail independently
launch { task2() } // Continues even if task1 fails
}
Testing Coroutines
class FeedViewModelTest {
@Test
fun `loadFeed updates state with notes`() = runTest {
val repository = mockk<FeedRepository>()
coEvery { repository.getFeed() } returns flowOf(testNotes)
val viewModel = FeedViewModel(repository)
viewModel.loadFeed()
advanceUntilIdle()
assertEquals(testNotes, viewModel.state.value.notes)
}
}
// Inject test dispatcher
val testDispatcher = StandardTestDispatcher()
Dispatchers.setMain(testDispatcher)
Nostr-Specific Patterns
Relay Connection Pool
class RelayPool(private val scope: CoroutineScope) {
private val relays = ConcurrentHashMap<String, RelayConnection>()
fun connect(url: String) {
scope.launch {
val connection = RelayConnection(url)
relays[url] = connection
supervisorScope {
launch { connection.receiveLoop() }
launch { connection.sendLoop() }
}
}
}
fun observeEvents(): Flow<Event> = relays.values
.map { it.events }
.merge()
.distinctBy { it.id }
}
Subscription Management
fun subscribe(filters: List<Filter>): Flow<Event> = channelFlow {
val subscriptionId = UUID.randomUUID().toString()
try {
relayPool.activeRelays.collect { relays ->
relays.forEach { relay ->
launch {
relay.subscribe(subscriptionId, filters)
.collect { send(it) }
}
}
}
} finally {
relayPool.unsubscribe(subscriptionId)
}
}
Agent Capabilities
-
Async Architecture Design
- Coroutine scope hierarchy
- Structured concurrency patterns
- Error propagation strategies
-
Flow Pipeline Design
- Cold vs hot stream selection
- Operator chaining
- Backpressure handling
-
Concurrency Patterns
- Parallel decomposition
- Rate limiting
- Resource pooling
-
Testing Strategies
- runTest usage
- Dispatcher injection
- Flow testing with Turbine
-
Performance Optimization
- Dispatcher selection
- Buffer sizing
- Cancellation efficiency
Scope Boundaries
In Scope
- kotlinx.coroutines library
- Flow/StateFlow/SharedFlow
- Channels and select
- Structured concurrency
- Exception handling
- Coroutine testing
- Dispatcher management
Out of Scope
- UI updates (use compose-ui agent)
- KMP configuration (use kotlin-multiplatform agent)
- Nostr protocol details (use nostr-protocol agent)