Audited all 143 plan files across the 10 plans/ folders. Each plan now carries a Status header (shipped | in-progress | queued | abandoned) backed by codebase evidence, and every folder has a README.md index grouping plans by status. Shipped plans were moved into a per-folder plans/archive/ (via git mv, history preserved) so each plans/ folder surfaces only live work: shipped (archived): 122 in-progress: 8 queued: 7 abandoned: 4 docs/plans/ is the frozen legacy folder; its plans were stamped and indexed in place (48 of 52 archived) but it remains closed to new plans. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016hpUivtmq4pgzqRbY6MYrA
4.4 KiB
Extract NestSubscriptionManager from NestViewModel
Status: in-progress — the stepping-stone
ActiveSubscription.ktis extracted, but the orchestration classNestSubscriptionManagerdoes not exist andreconcileSubscriptions/openSubscription/closeSubscription/fetchSpeakerCatalogstill live in the 2295-lineNestViewModel. Audited 2026-06-30.
Status: deferred — flagged in the audit pass, not landed yet.
Why
NestViewModel.kt is 2112 lines and growing. ~1000 of those lines are
the per-speaker subscription-lifecycle state machine: open / close /
reconcile / catalog-fetch / decoder-await / level-tap / mute-routing /
hush. The rest is room-level public API (connect, disconnect,
broadcast, presence, reactions, focus / network observers, UI state).
These two concerns are coupled by shared mutable state but do not share a responsibility. The audit-9 finding flagged it as the single biggest SRP breach in the touched code.
We extracted ActiveSubscription into its own file (ActiveSubscription.kt)
in commit <TBD> as a stepping stone — the deferred extraction
proper is the orchestration class.
Target shape
internal class NestSubscriptionManager(
private val viewModelScope: CoroutineScope,
private val signer: NostrSigner,
private val decoderFactory: (channelCount: Int, sampleRate: Int) -> OpusDecoder,
private val playerFactory: (channelCount: Int, sampleRate: Int) -> AudioPlayer,
private val onActiveSpeakersChanged: (Set<String>) -> Unit,
private val onSpeakerActivity: (String) -> Unit,
private val onAudioLevel: (String, Float) -> Unit,
private val onConnectingSpeakerChanged: (pubkey: String, connecting: Boolean) -> Unit,
private val effectiveListenMuted: () -> Boolean,
private val locallyHushed: () -> Set<String>,
) {
val speakerCatalogs: StateFlow<Map<String, RoomSpeakerCatalog>>
val audioLevels: StateFlow<Map<String, Float>>
fun bind(listener: NestsListener)
fun unbind() // cancel everything; release native resources
fun updateSpeakers(requested: Set<String>)
fun applyEffectiveMute()
fun setLocalHushed(pubkey: String, hushed: Boolean)
}
State that moves
From NestViewModel:
activeSubscriptions: MutableMap<String, ActiveSubscription>catalogJobs: MutableMap<String, Job>_speakerCatalogs: MutableStateFlow<Map<String, RoomSpeakerCatalog>>requestedSpeakers: Set<String>speakingExpiryJobs: MutableMap<String, Job>(foronSpeakerActivitydebounce)_audioLevelsif separable
Methods that move
reconcileSubscriptionsopenSubscription(~150 lines)closeSubscriptionfetchSpeakerCatalogawaitAudioPipelineConfigonSpeakerActivitydebounce timeronAudioLevelcoalescingapplyEffectiveListenMute's subscription-touching halfsetLocalHushed's player.setVolume halfpublishActiveSpeakers
What stays in NestViewModel
- Public lifecycle (
connect,disconnect,onCleared) - Connection state machine (
ConnectionUiState) - Broadcast state (
broadcast/_uiState.broadcast) - Presence aggregation (kind 10312 events)
- Reactions
- Focus / network observers
- The
NestUiStatecomposition itself
NestViewModel calls manager.bind(listener) on each fresh
listener and manager.unbind() on disconnect / cleanup.
updateSpeakers and mute / hush propagate through to the manager.
Why deferred
- 1000-line code move across ~10 methods + 5 state fields
- Subtle coupling: catalog readiness affects spinner state, mute
state has effective + per-speaker flavours, expiry jobs need
parent scope,
closedflag is currently a single VM-wide flag - Test surface:
NestViewModelTestexercises subscription paths through the VM today; would need to either keep that surface or add aNestSubscriptionManagerTest.
The extraction has a clean contract (callbacks for the bits that remain VM-side) but landing it without behavioural drift wants a focused review pass plus its own dedicated test rebuild — bigger than fits in the current audit-pass.
When to land
After:
- The catalog-driven decoder reconfig (T3) has run in production
for long enough that the
awaitAudioPipelineConfigpattern is proven against real publishers. - Any pending subscription-lifecycle bug fixes (e.g. the audit's earlier "boundary-rebuild dangling decoder" path) are settled — moving them mid-fix risks introducing regressions.
Track as a follow-up issue rather than a near-term must-do.