---
name: kotlin-coroutines
description: Coroutines/Flow in Amethyst: picking IO vs Default, scopes and cancellation (CancellationException, withTimeout), StateFlow/SharedFlow/Channel and stateIn choices, runTest. Use when writing or reviewing async Kotlin code.
---

# Kotlin coroutines in Amethyst

Repo-specific rules only. Textbook coroutines/Flow knowledge is assumed.

## Dispatchers

**`Dispatchers.IO` is the default; `Default` only for provably lock-free CPU work.**
The full rule, with the reasons and examples (`ParallelEventVerifier`,
`PoWMiner.MiningDispatcher`, `BagSweep`, `NostrSignerExternal`), is
`commons/ARCHITECTURE.md` › "Choosing a dispatcher". Read it before writing
`flowOn`/`withContext`. In `commonMain`, `Dispatchers.IO` needs
`import kotlinx.coroutines.IO`.

## Relay work: reuse, don't hand-roll

Never write your own `callbackFlow { openReqSubscription(...); awaitClose { ... } }`
REQ loop. One-shot and paged fetches, publish-and-confirm, COUNT and negentropy sync
already exist as `INostrClient` extensions in
`quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/`
(catalog: its `README.md`; flow helpers in `../reqs/`). Their timeouts are idle
windows (`idleTimeoutMs`), not deadlines. For loads tied to a composable's lifetime,
see the `relay-client` skill.

`callbackFlow` is still the right bridge for platform callbacks; copy
`amethyst/.../service/connectivity/ConnectivityFlow.kt` (initial emit, `awaitClose`
unregister, `distinctUntilChanged().debounce(200).flowOn(Dispatchers.IO)`).

## Scopes

- **Stored scopes are fine for lifecycle owners.** `Account` takes
  `scope: CoroutineScope`; Android services build
  `CoroutineScope(Dispatchers.IO + SupervisorJob() + exceptionHandler)` and
  `cancel()` it in `onDestroy` (`PushNotificationReceiverService`). Hundreds of
  such fields are legitimate; don't flag them on sight.
- **New non-owner classes** (helpers, repositories, parsers) expose `suspend` APIs
  and do not `launch` from `init`. Let the owner pick the scope.
- **No `runBlocking`** outside tests, previews and the CLI entry point. No
  `GlobalScope`.

## Cancellation and timeouts

- A broad `catch (e: Exception)` / `runCatching` around a suspend call must rethrow
  `CancellationException`.
- **`TimeoutCancellationException` IS a `CancellationException`.** A blanket
  rethrow turns one slow RPC into a dead loop: rethrowing it ended the cordn sync
  loop for the whole session. Either catch the timeout narrowly, right at the
  `withTimeout`, or rethrow only when the coroutine itself is cancelled
  (`if (!isActive) throw e`). See cordn-kmp's `cordn/client/CordnSyncLoop.kt` for both.

## State and events

- `MutableStateFlow.update { }` instead of `_s.value = _s.value.copy(...)`. Keep the
  block pure and cheap (it can retry); build expensive values before it.
- `stateIn`/`shareIn` belong on a `val`, never inside a function (each call starts a
  new sharing coroutine).
- `WhileSubscribed` stops upstream with no collectors, so `.value` goes stale or
  stays at the initial value. If code reads `.value` synchronously, use `Eagerly`.
- `stateFlow.map { }` is a plain `Flow`; terminate with `stateIn` if consumers
  need `.value`.
- One-shot events for a single consumer (navigation, a toast): a
  `Channel(Channel.BUFFERED).receiveAsFlow()`, not a no-replay `MutableSharedFlow`
  that drops emissions with no collector. A `Channel` delivers each item to ONE
  collector; if every observer must see it, model it as state.
- Don't invent fake domain sentinels as the initial value; use `null` or a sealed
  loading state.

## Tests

- `runTest` from `kotlinx-coroutines-test`, not `runBlocking`. Turbine is not a
  dependency; collect into a list in `backgroundScope.launch { flow.collect { } }`
  and drive time with `advanceUntilIdle()`, `runCurrent()`, `advanceTimeBy()`.
- Inject the dispatcher/scope under test (`StandardTestDispatcher(testScheduler)`
  or `UnconfinedTestDispatcher`) so delays run on virtual time.
- Never wait on wall-clock time (`delay(350)` racing a 200 ms timeout flaked on
  loaded CI). Wait on the test scheduler or a condition.


---
Source attribution: vitorpamplona
Original project: https://github.com/vitorpamplona/amethyst/blob/3cbe201baa37fb15cd81cfff6800ab97c7b4a1f9/.claude/skills/kotlin-coroutines/SKILL.md
License: MIT
License information: https://opensource.org/license/mit
Original upstream instructions; downloaded from Mega AI Hub.

## Upstream license notice

MIT License

Copyright (c) 2023 Vitor Pamplona

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.



# Mega AI Hub resource metadata

Creator / publisher: vitorpamplona
Original project: https://github.com/vitorpamplona/amethyst/blob/3cbe201baa37fb15cd81cfff6800ab97c7b4a1f9/.claude/skills/kotlin-coroutines/SKILL.md
Attribution basis: Source repository record

Fields of work: Development & Engineering, Product Management
Roles: Backend Developer, DevOps / SRE, Full-Stack Developer, Mobile Developer, Product Manager
Specialties: Backend, Mobile
Tools: Amethyst
Country applicability: Country not specified
Classification basis: Source catalog labels
Instruction SHA-256: 7055d9cb9519919f9364ac97459f9b6e02a457cea5c23b9f5fa99c3a02666ea2
