Skip to content

refactor: Extract explosion handling into ExplosionCoordinator#526

Closed
yewreeka wants to merge 1 commit into
devfrom
refactor/extract-explosion-viewmodel
Closed

refactor: Extract explosion handling into ExplosionCoordinator#526
yewreeka wants to merge 1 commit into
devfrom
refactor/extract-explosion-viewmodel

Conversation

@yewreeka

@yewreeka yewreeka commented Feb 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Extract explosion-related code from ConversationViewModel into a dedicated ExplosionCoordinator class for better separation of concerns.

Changes

New file: ExplosionCoordinator.swift

  • Owns ExplodeState management
  • Manages explosion task lifecycle
  • Provides explode() and scheduleExplosion() operations
  • Self-contained, testable unit

Updated: ConversationViewModel.swift

  • Holds ExplosionCoordinator as a dependency
  • Forwards explodeState as computed property
  • Delegates explosion actions to coordinator
  • Removes direct explosion task management

Benefits

  • Testability: Explosion logic can be tested in isolation
  • Separation of concerns: ConversationViewModel is ~50 lines smaller
  • Clarity: Explosion behavior is encapsulated in one place

Part of

Tech debt paydown plan for post-1.1.0 release (see PR #525 for documentation).

Note

Extract explosion orchestration into ExplosionCoordinator and update ConversationViewModel to delegate explode and scheduleExplosion to ExplosionCoordinator in ConversationViewModel.swift and add new coordinator in ExplosionCoordinator.swift

Move explosion state and task management from ConversationViewModel to ExplosionCoordinator, proxy explodeState through coordinator, and route explodeConvo and scheduleExplosion to coordinator APIs.

📍Where to Start

Start with the ExplosionCoordinator class and its explode and scheduleExplosion methods in ExplosionCoordinator.swift. Then see how ConversationViewModel delegates in ConversationViewModel.swift.

Macroscope summarized 83c6f30.

Extract explosion-related code from ConversationViewModel into a dedicated
ExplosionCoordinator class for better separation of concerns.

Changes:
- Create ExplosionCoordinator class that owns:
  - ExplodeState management
  - Explosion task lifecycle
  - explode() and scheduleExplosion() operations
- Update ConversationViewModel to:
  - Hold ExplosionCoordinator as a dependency
  - Forward explodeState as computed property
  - Delegate explosion actions to coordinator

This reduces ConversationViewModel complexity and makes explosion logic
testable in isolation.

Part of tech debt paydown plan for post-1.1.0 release.
@claude

claude Bot commented Feb 25, 2026

Copy link
Copy Markdown

Claude finished @yewreeka's task —— View job


Code Review Complete ✅

Tasks

  • Read changed files
  • Analyze code quality and best practices
  • Check for potential bugs
  • Review performance considerations
  • Assess security concerns
  • Evaluate test coverage

Summary

This refactoring successfully extracts explosion logic into a dedicated coordinator, improving separation of concerns. The implementation is solid with good error handling and task lifecycle management. A few minor improvements are recommended below.


Code Quality & Best Practices

✅ Strengths:

  • Clean separation of concerns - ExplosionCoordinator is now a focused, single-responsibility class
  • Proper use of @Observable and @MainActor following modern Swift concurrency patterns
  • Good task lifecycle management with cancellation in deinit
  • Consistent with project conventions (dependency injection via protocols, use of @ObservationIgnored)

⚠️ Recommendations:

  1. ExplosionCoordinator.swift:16-18 - Consider making explosionWriter dependency injectable via protocol:

    // Current: Protocol already used ✅
    init(explosionWriter: any ConversationExplosionWriterProtocol)

    This is already good practice for testability.

  2. Computed property forwarding - ConversationViewModel.swift:213 forwards explodeState:

    var explodeState: ExplodeState { explosionCoordinator.state }

    This is a clean approach that maintains the existing API surface while delegating to the coordinator.


Potential Bugs & Issues

⚠️ Minor Issue - State Machine Guard Logic:

ExplosionCoordinator.swift:30 and ExplosionCoordinator.swift:68 have slightly different guard conditions:

// explode() allows error OR scheduled
guard state.isReady || state.isError || state.isScheduled else { return }

// scheduleExplosion() only allows error
guard state.isReady || state.isError else { return }

Question: Should scheduleExplosion() also allow transitioning from .scheduled state? If a user schedules an explosion, then changes the time, it seems reasonable to allow rescheduling. Consider:

guard state.isReady || state.isError || state.isScheduled else { return }

⚠️ Task Cancellation Race Condition:

Both methods follow this pattern:

explodeTask?.cancel()
explodeTask = Task { [weak self] in
    guard let self else { return }
    // async work...
}

This creates a potential race: the previous task is cancelled, but the new task starts immediately. If the previous task hasn't finished its error handling (lines 56-58 in explode()), there could be state conflicts. Consider using Task.yield() after cancellation or storing the previous task and awaiting its cancellation.


Performance Considerations

✅ Good:

  • Task cancellation prevents unnecessary work when operations are superseded
  • [weak self] captures prevent retain cycles
  • State transitions are synchronous (no async gaps)

ℹ️ Note:

  • The 2-second error display delay (ExplosionCoordinator.swift:56,92) is reasonable for UX but means the coordinator holds state longer. Not a concern for typical usage patterns.

Security Concerns

✅ No security issues identified:

  • No sensitive data exposed in logs (only conversation IDs, which are internal references)
  • Proper error handling without leaking implementation details
  • No force unwrapping or unsafe operations

Test Coverage

⚠️ Testing Recommendations:

The ExplosionCoordinator is now independently testable. Suggested test cases:

  1. State Transitions:

    • Test all valid state transitions (.ready.exploding.exploded)
    • Test guard conditions prevent invalid state transitions
    • Test error state recovery after 2-second delay
  2. Task Lifecycle:

    • Verify task cancellation on deinit
    • Verify task cancellation when new operation starts
    • Test [weak self] behavior when coordinator is deallocated mid-operation
  3. Error Handling:

    • Mock explosionWriter failures
    • Verify error state is set with correct message
    • Verify recovery to .ready state after delay
  4. Integration with ConversationViewModel:

    • Verify computed property forwarding works correctly
    • Test that callbacks (onExploded, onImmediateExplosion) are invoked

Example test structure:

@MainActor
final class ExplosionCoordinatorTests: XCTestCase {
    var coordinator: ExplosionCoordinator!
    var mockWriter: MockConversationExplosionWriter!
    
    override func setUp() async throws {
        mockWriter = MockConversationExplosionWriter()
        coordinator = ExplosionCoordinator(explosionWriter: mockWriter)
    }
    
    func testExplodeTransitionsToExplodingState() async {
        // Given: coordinator in ready state
        XCTAssertTrue(coordinator.state.isReady)
        
        // When: explode is called
        coordinator.explode(/* ... */)
        
        // Then: state transitions to exploding
        XCTAssertEqual(coordinator.state, .exploding)
    }
}

Additional Notes

  1. Line count reduction: The ViewModel went from ~1195 lines to ~1125 lines (70 lines removed), while the new coordinator is 98 lines. Net reduction of ~50 lines of complexity in the ViewModel.

  2. Consistency with CLAUDE.md conventions:

    • Uses @Observable pattern correctly
    • Follows dependency injection via protocols
    • Proper use of @MainActor and @ObservationIgnored
    • Clean naming conventions
  3. Future improvements: Consider extracting other coordinators from ConversationViewModel using the same pattern (e.g., OnboardingCoordinator is already partially extracted).


Verdict

Approved with minor recommendations. This is a clean refactoring that improves testability and maintainability. The identified issues are minor and non-blocking:

  • State machine logic is consistent enough for current use case
  • Task cancellation race is theoretical and unlikely to cause issues in practice
  • Test coverage recommendations are for future work

Great job on reducing complexity in ConversationViewModel! 🎉


yewreeka added a commit that referenced this pull request Feb 25, 2026
- Mark items 1, 2, 4 as complete
- Add audit findings for UnusedConversationCache (no issues found)
- Link PRs #525 and #526
yewreeka added a commit that referenced this pull request Feb 25, 2026
* docs: Add ADR 011 - Inbox Lifecycle Architecture

Comprehensive documentation of the inbox lifecycle management system including:
- Component overview and relationships
- State diagrams for InboxLifecycleManager, InboxStateMachine, ConversationStateMachine
- Capacity management and LRU eviction
- Pending invite protection rules
- Key flows (open conversation, create new, app launch)
- Edge cases and race condition handling

Part of tech debt paydown plan for post-1.1.0 release.

* docs: Coalesce inbox lifecycle docs into ADR 003

Merge new state machine documentation into existing ADR 003 instead of
creating a separate ADR 011.

Added to ADR 003:
- Architecture overview diagram showing component relationships
- InboxStateMachine state diagram (idle → authorizing → ready → backgrounded)
- ConversationStateMachine state diagram (uninitialized → creating/joining → ready)
- Edge cases section (re-wake during sleep, duplicate registration, stale data)
- Additional related files

Removed:
- ADR 011 (duplicate/overlapping content)

* docs: Update tech debt plan with audit results

- Mark items 1, 2, 4 as complete
- Add audit findings for UnusedConversationCache (no issues found)
- Link PRs #525 and #526

* docs: Mark item 5 complete - inbox state tests already comprehensive

Audit found 4,341 lines of existing test coverage across:
- InboxLifecycleManagerTests (1,596 lines)
- InboxStateMachineTests (869 lines)
- ConversationStateMachineTests (1,876 lines)
- SleepingInboxMessageCheckerIntegrationTests (Docker-based)

The flaky test fixes were about test isolation and timing,
not missing coverage.

* docs: Finalize tech debt plan - most items were non-issues

Final tally: 2 of 6 items actually needed work
- Item 3 dropped (photo extraction risk > benefit)
- Items 4, 5 were already in good shape
- Item 6 deferred (still behind feature flag)

Added retrospective with lessons learned.

* docs: Clarify item 6 and add QAAutomationServer rename to retrospective

Item 6 is about the assistant/agent feature (#476, #503), not the QA
automation server which was confusingly named AgentServer.

* chore: Remove tech debt plan from PR
@yewreeka

Copy link
Copy Markdown
Contributor Author

Closing - the extraction doesn't provide enough value to justify the churn.

@yewreeka yewreeka closed this Feb 25, 2026
@yewreeka yewreeka deleted the refactor/extract-explosion-viewmodel branch February 25, 2026 18:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant