-
-
Notifications
You must be signed in to change notification settings - Fork 226
Description
Enhancement
To have a reliable Realtime experience, the client should handle connection issues by reconnecting to the socket and resubscribing to channels when it detects that the internet connection has been reestablished.
Use NWPathMonitor on Apple platforms.
Problem
The current Realtime implementation uses passive detection of network issues - it only discovers connectivity loss when WebSocket operations fail or heartbeats timeout (up to 25 seconds delay). This results in:
- Delayed detection: Up to 25 seconds before heartbeat timeout triggers reconnection
- No proactive reconnection: When network is restored, the SDK waits for the next scheduled operation to fail/succeed
- Poor user experience: Users may think the app is stuck when network returns but connection hasn't re-established
Current Behavior
| Event | Detection Method | Delay |
|---|---|---|
| Network lost | WebSocket error or heartbeat timeout | 0-25 seconds |
| Network restored | Next reconnection attempt succeeds | 7+ seconds (reconnect delay) |
Desired Behavior
| Event | Detection Method | Delay |
|---|---|---|
| Network lost | NWPathMonitor callback | Immediate |
| Network restored | NWPathMonitor callback | Immediate reconnect |
Root Cause
The SDK has no active network monitoring. All network-related code is reactive:
Sources/Realtime/WebSocket/URLSessionWebSocket.swift:214-231- Maps network errors to WebSocket close codes (reactive)Sources/Realtime/RealtimeClientV2.swift:291-301- Reconnection only triggers on close/error eventsSources/Realtime/RealtimeClientV2.swift:446-506- Heartbeat provides health checks but with 25-second intervals
No NWPathMonitor or Network.framework usage exists anywhere in the codebase.
Proposed Solution
Approach: Implement NetworkMonitor using NWPathMonitor
Create a new component that monitors network connectivity and triggers immediate reconnection when connectivity is restored.
Implementation Plan
1. Create NetworkMonitor class
New file: Sources/Realtime/NetworkMonitor.swift
#if canImport(Network)
import Network
@available(iOS 12.0, macOS 10.14, tvOS 12.0, watchOS 5.0, *)
final class NetworkMonitor: Sendable {
enum Status: Sendable {
case satisfied // Network available
case unsatisfied // No network
case requiresConnection // Network requires user action (captive portal)
}
private let monitor: NWPathMonitor
private let queue = DispatchQueue(label: "com.supabase.realtime.networkmonitor")
private let statusSubject = AsyncValueSubject<Status>(.unsatisfied)
var status: Status { statusSubject.value }
var statusChange: AsyncStream<Status> { statusSubject.values }
init(requiredInterfaceType: NWInterface.InterfaceType? = nil) {
if let interfaceType = requiredInterfaceType {
monitor = NWPathMonitor(requiredInterfaceType: interfaceType)
} else {
monitor = NWPathMonitor()
}
monitor.pathUpdateHandler = { [weak self] path in
let newStatus: Status
switch path.status {
case .satisfied:
newStatus = .satisfied
case .unsatisfied:
newStatus = .unsatisfied
case .requiresConnection:
newStatus = .requiresConnection
@unknown default:
newStatus = .unsatisfied
}
self?.statusSubject.yield(newStatus)
}
}
func start() {
monitor.start(queue: queue)
}
func cancel() {
monitor.cancel()
}
}
#endif2. Add configuration option
In Sources/Realtime/Types.swift, add to RealtimeClientOptions:
/// Whether to monitor network connectivity and trigger immediate reconnection
/// when connectivity is restored. Uses NWPathMonitor on Apple platforms.
/// Default: true on supported Apple platforms, false elsewhere.
public var monitorNetworkConnectivity: Bool = defaultMonitorNetworkConnectivity
#if os(iOS) || os(macOS) || os(tvOS) || os(watchOS) || os(visionOS)
private static let defaultMonitorNetworkConnectivity = true
#else
private static let defaultMonitorNetworkConnectivity = false
#endif3. Integrate into RealtimeClientV2
In Sources/Realtime/RealtimeClientV2.swift:
// Add to MutableState
#if canImport(Network)
@available(iOS 12.0, macOS 10.14, tvOS 12.0, watchOS 5.0, *)
var networkMonitor: NetworkMonitor?
var networkMonitorTask: Task<Void, Never>?
#endif
// Add to initializer (after other setup)
#if canImport(Network)
if #available(iOS 12.0, macOS 10.14, tvOS 12.0, watchOS 5.0, *),
options.monitorNetworkConnectivity {
setupNetworkMonitoring()
}
#endif
// New method
#if canImport(Network)
@available(iOS 12.0, macOS 10.14, tvOS 12.0, watchOS 5.0, *)
private func setupNetworkMonitoring() {
let monitor = NetworkMonitor()
mutableState.withValue {
$0.networkMonitor = monitor
$0.networkMonitorTask = Task { [weak self] in
var previousStatus: NetworkMonitor.Status?
for await status in monitor.statusChange {
guard let self else { return }
// Trigger reconnection when network transitions to satisfied
if status == .satisfied && previousStatus != .satisfied {
// Only reconnect if we were disconnected
if self.status == .disconnected {
await self.connect()
}
}
previousStatus = status
}
}
}
monitor.start()
}
#endif
// Add cleanup to deinit/disconnect
#if canImport(Network)
if #available(iOS 12.0, macOS 10.14, tvOS 12.0, watchOS 5.0, *) {
mutableState.withValue {
$0.networkMonitorTask?.cancel()
$0.networkMonitor?.cancel()
}
}
#endif4. Expose network status to apps
Add public API for apps that want to observe network status:
/// Current network connectivity status.
/// Available on iOS 12+, macOS 10.14+, tvOS 12+, watchOS 5+.
#if canImport(Network)
@available(iOS 12.0, macOS 10.14, tvOS 12.0, watchOS 5.0, *)
public var networkStatus: NetworkMonitor.Status? {
mutableState.networkMonitor?.status
}
@available(iOS 12.0, macOS 10.14, tvOS 12.0, watchOS 5.0, *)
public var networkStatusChange: AsyncStream<NetworkMonitor.Status>? {
mutableState.networkMonitor?.statusChange
}
#endifFiles to Change
-
Sources/Realtime/NetworkMonitor.swift- NEW - NWPathMonitor wrapper -
Sources/Realtime/Types.swift:15-67- AddmonitorNetworkConnectivityoption -
Sources/Realtime/RealtimeClientV2.swift:34-51- Add network monitor to MutableState -
Sources/Realtime/RealtimeClientV2.swift:120-168- Initialize and start network monitoring -
Sources/Realtime/RealtimeClientV2.swift:512-533- Cleanup in disconnect/deinit -
Tests/RealtimeTests/- Add tests for network monitoring
Implementation Notes
- Platform availability: NWPathMonitor requires iOS 12+, macOS 10.14+, tvOS 12+, watchOS 5+ - use
@availableand#if canImport(Network) - Thread safety: NWPathMonitor callbacks come on custom dispatch queue; use async/await bridge
- Debouncing: Network status can fluctuate rapidly - may need debouncing to avoid reconnect storms
- Integration with SDK-652: This complements background/foreground handling - both can work together
- Captive portals: Handle
.requiresConnectionstatus appropriately (don't attempt reconnect)
Edge Cases
- Network fluctuates rapidly (wifi to cellular transitions) - debounce reconnection attempts
- Captive portal detected - don't attempt reconnection until resolved
- Already connecting when network status changes - don't double-connect
- Monitor started but connection intentionally disconnected by user - respect user intent
- VPN connects/disconnects - should trigger path update
Relationship to SDK-652
| Issue | Trigger | Use Case |
|---|---|---|
| SDK-652 | App lifecycle (background/foreground) | iOS suspends WebSocket when backgrounded |
| SDK-653 (this) | Network connectivity change | Airplane mode, wifi loss, cellular handoff |
Both issues can share infrastructure (e.g., RealtimeLifecycleManager) but address different scenarios.
Acceptance Criteria
-
NWPathMonitorused on supported Apple platforms to detect network changes - Immediate reconnection triggered when network transitions from unavailable to available
- No reconnection attempt when network remains unavailable
- Respects
.requiresConnectionstatus (captive portals) - does not attempt blind reconnect - Behavior can be disabled via
RealtimeClientOptions.monitorNetworkConnectivity = false - Network status exposed via public API for app-level UI updates
- Works on iOS 12+, macOS 10.14+, tvOS 12+, watchOS 5+, visionOS 1+
- Graceful degradation on Linux/unsupported platforms (no-op)
- Unit tests cover network status transitions
- No memory leaks (proper cleanup of NWPathMonitor)
- Channels automatically rejoin after network-triggered reconnection (existing behavior)
Testing Strategy
Unit Tests
func testNetworkRestoredTriggersReconnect() async {
let client = RealtimeClientV2(...)
await client.connect()
// Simulate network loss (would need mock NetworkMonitor)
// ...
// Simulate network restored
// ...
// Verify reconnection was triggered
XCTAssertEqual(client.status, .connected)
}
func testNoReconnectOnCaptivePortal() async {
// When network status is .requiresConnection
// Should NOT attempt automatic reconnection
}Manual Testing
- Connect to Realtime channel
- Enable Airplane mode
- Verify connection status changes to disconnected
- Disable Airplane mode
- Verify immediate reconnection (not waiting for heartbeat timeout)
- Verify channel events resume
Network Condition Simulator
Use Xcode's Network Link Conditioner or networksetup CLI to simulate:
- 100% packet loss (network unavailable)
- Restore to normal (network available)
- Verify reconnection timing is immediate vs. ~25 seconds without this feature
Research
Related Issues
- GitHub: #579
- Linear: SDK-652 (Background/foreground handling - complementary)
- Linear: SDK-277 (JS SDK app state handling)
Apple Documentation
Existing Reconnection Infrastructure
The SDK already has robust reconnection mechanics:
RealtimeClientV2.swift:291-301-reconnect()methodRealtimeClientV2.swift:397-403- Channel auto-rejoinRealtimeClientV2.swift:589-626- Send buffer mechanism
This enhancement just needs to call the existing connect() at the right time.
Current Network Error Handling
Already maps these URLSession errors to WebSocket close codes (URLSessionWebSocket.swift:214-231):
NSURLErrorTimedOut-> 1006NSURLErrorNetworkConnectionLost-> 1006NSURLErrorNotConnectedToInternet-> 1006
Generated with Claude Code /issue