Skip to content

refactor: replace Combine WebSocket streams with AsyncStream - #16

Open
Sandesh282 wants to merge 4 commits into
mainfrom
feature/asyncstream-websocket
Open

Sandesh282 wants to merge 4 commits into
mainfrom
feature/asyncstream-websocket

Conversation

@Sandesh282

Copy link
Copy Markdown
Owner

Description

The previous WebSocket layer used Combine PassthroughSubject / CurrentValueSubject to propagate live events from LiveWebSocketService up through the repository and ViewModel layers. This created a mixed-concurrency model where Swift Concurrency (actor, async/await) owned all state, but Combine owned all event propagation — forcing callers to bridge between the two worlds with .sink + Task { } wrappers that are easy to get wrong.

This PR removes that split entirely. The entire event path is now 100% pure Swift Concurrency.


Changes

WebSocketService.swift — Protocol

  • events: AnyPublisher<WebSocketEvent, Never>eventStream: AsyncStream<WebSocketEvent>
  • connectionState: AnyPublisher<...>connectionStateStream: AsyncStream<WebSocketConnectionState>
  • WebSocketEvent and WebSocketConnectionState now conform to Sendable so they can safely cross actor boundaries inside continuations

LiveWebSocketService.swift — Concrete Implementation

  • Removes PassthroughSubject / CurrentValueSubject
  • Replaces with a [UUID: Continuation] dictionary pattern guarded by NSLock — each call to eventStream returns an independent AsyncStream backed by its own continuation, so multiple concurrent subscribers (e.g. ContestRepository + SubmissionRepository) don't interfere with each other
  • connectionStateStream yields the current state immediately on subscription (cold observable equivalent of CurrentValueSubject)

Repositories — ProfileRepository, ContestRepository, SubmissionRepository

  • Remove import Combine
  • Replace bindWebSocketEvents() Combine .sink closures with withTaskGroup + for await event in wsService.eventStream loops
  • Manual throttling (1s for ratings, 500ms for verdicts) replaces .throttle(for:scheduler:latest:)
  • Each repository now vends its own AsyncStream properties (e.g. ratingUpdated, verdictReceived) backed by the same continuation dictionary pattern

ViewModels — ContestViewModel, ProfileViewModel

  • Remove import Combine
  • Replace Set<AnyCancellable> + .sink/.assign with [Task<Void, Never>] observation tasks
  • All UI state mutations hop to MainActor.run { } explicitly, making the threading contract visible in code

Before / After

Before After
Event transport AnyPublisher (Combine) AsyncStream (Swift Concurrency)
Combine imports 6 files 0 files
Subscriber pattern .sink + store(in:) for await loop in Task
Thread dispatch .receive(on: DispatchQueue.main) await MainActor.run { }
Multi-subscriber support Shared subject (single multicast) Independent continuation per subscriber

Testing

Build-verified on Xcode (simulator). No functional behaviour changes — this is a pure infrastructure refactor. Existing SwiftUI previews and app flow unchanged.

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