diff --git a/lib/providers/stream_recovery.dart b/lib/providers/stream_recovery.dart index 1df4d550..fc8f29df 100644 --- a/lib/providers/stream_recovery.dart +++ b/lib/providers/stream_recovery.dart @@ -329,12 +329,22 @@ class StreamRecoveryWrapper { _startRealtimeSubscription(); } - /// Clean up all resources. + /// Clean up all resources and notify the status callback that this + /// channel is no longer active, preventing ghost entries in the + /// [RealtimeController]'s recovering-channels set. void dispose() { + if (_disposed) return; _disposed = true; _pollingTimer?.cancel(); _recoveryTimer?.cancel(); _realtimeSub?.cancel(); + // Ensure the channel is removed from the recovering set when the + // wrapper is torn down (e.g. provider disposed during navigation). + // Without this, disposed wrappers that were mid-recovery leave + // orphaned entries that keep the reconnection indicator spinning. + if (_connectionStatus != StreamConnectionStatus.connected) { + _onStatusChanged?.call(channelName, StreamConnectionStatus.connected); + } _controller?.close(); } } diff --git a/lib/providers/tasks_provider.dart b/lib/providers/tasks_provider.dart index 2d6ee16b..d00c461e 100644 --- a/lib/providers/tasks_provider.dart +++ b/lib/providers/tasks_provider.dart @@ -261,7 +261,7 @@ final tasksProvider = StreamProvider>((ref) { // ── Realtime stream ─────────────────────────────────────────────────────── // Processes every realtime event through the same isolate. Debounced so // rapid consecutive events (e.g. bulk inserts) don't cause repeated renders. - wrapper.stream + final wrapperSub = wrapper.stream .asyncMap((result) async { final payload = _buildTaskPayload( tasks: result.data, @@ -286,10 +286,12 @@ final tasksProvider = StreamProvider>((ref) { }, onError: (Object e) { debugPrint('[tasksProvider] stream error: $e'); - controller.addError(e); + // Don't forward errors — the wrapper handles recovery internally. }, ); + ref.onDispose(wrapperSub.cancel); + return controller.stream; }); diff --git a/lib/providers/tickets_provider.dart b/lib/providers/tickets_provider.dart index b2415c35..d3eacd83 100644 --- a/lib/providers/tickets_provider.dart +++ b/lib/providers/tickets_provider.dart @@ -256,7 +256,7 @@ final ticketsProvider = StreamProvider>((ref) { // ── Realtime stream ─────────────────────────────────────────────────────── // Processes every realtime event through the same isolate. Debounced so // rapid consecutive events (e.g. bulk inserts) don't cause repeated renders. - wrapper.stream + final wrapperSub = wrapper.stream .asyncMap((result) async { final payload = _buildTicketPayload( tickets: result.data, @@ -280,10 +280,12 @@ final ticketsProvider = StreamProvider>((ref) { }, onError: (Object e) { debugPrint('[ticketsProvider] stream error: $e'); - controller.addError(e); + // Don't forward errors — the wrapper handles recovery internally. }, ); + ref.onDispose(wrapperSub.cancel); + return controller.stream; });