Fix flickering issues due to reconnecting loop
This commit is contained in:
@@ -94,8 +94,12 @@ class RealtimeController extends ChangeNotifier {
|
||||
/// Convenience callback suitable for [StreamRecoveryWrapper.onStatusChanged].
|
||||
///
|
||||
/// Routes [StreamConnectionStatus] to the appropriate mark method.
|
||||
/// Both `connected` and `polling` are treated as "recovered" because
|
||||
/// polling is a functional fallback that still delivers data — the user
|
||||
/// doesn't need to see a reconnection indicator while data flows via REST.
|
||||
void handleChannelStatus(String channel, StreamConnectionStatus status) {
|
||||
if (status == StreamConnectionStatus.connected) {
|
||||
if (status == StreamConnectionStatus.connected ||
|
||||
status == StreamConnectionStatus.polling) {
|
||||
markChannelRecovered(channel);
|
||||
} else {
|
||||
markChannelRecovering(channel);
|
||||
|
||||
@@ -109,6 +109,7 @@ class StreamRecoveryWrapper<T> {
|
||||
int _recoveryAttempts = 0;
|
||||
Timer? _pollingTimer;
|
||||
Timer? _recoveryTimer;
|
||||
Timer? _stabilityTimer;
|
||||
StreamSubscription<List<Map<String, dynamic>>>? _realtimeSub;
|
||||
StreamController<StreamRecoveryResult<T>>? _controller;
|
||||
bool _disposed = false;
|
||||
@@ -159,8 +160,28 @@ class StreamRecoveryWrapper<T> {
|
||||
|
||||
void _onRealtimeData(List<Map<String, dynamic>> rows) {
|
||||
if (_disposed) return;
|
||||
// Successful data — reset recovery counters.
|
||||
_recoveryAttempts = 0;
|
||||
|
||||
// When recovering, don't reset _recoveryAttempts immediately.
|
||||
// Supabase streams emit an initial REST fetch before the realtime
|
||||
// channel is established. If the channel keeps failing, resetting
|
||||
// on that REST data creates an infinite loop (data → reset → subscribe
|
||||
// → fail → data → reset …). Instead, start a stability timer — only
|
||||
// reset after staying connected without errors for 30 seconds.
|
||||
if (_recoveryAttempts > 0) {
|
||||
_stabilityTimer?.cancel();
|
||||
_stabilityTimer = Timer(const Duration(seconds: 30), () {
|
||||
if (!_disposed) {
|
||||
_recoveryAttempts = 0;
|
||||
debugPrint(
|
||||
'StreamRecoveryWrapper[$channelName]: '
|
||||
'connection stable for 30s — recovery counter reset',
|
||||
);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
_recoveryAttempts = 0;
|
||||
}
|
||||
|
||||
_setStatus(StreamConnectionStatus.connected);
|
||||
_emit(
|
||||
StreamRecoveryResult<T>(
|
||||
@@ -174,6 +195,9 @@ class StreamRecoveryWrapper<T> {
|
||||
void _onRealtimeError(Object error, [StackTrace? stack]) {
|
||||
if (_disposed) return;
|
||||
|
||||
// Cancel any stability timer — the connection is not stable.
|
||||
_stabilityTimer?.cancel();
|
||||
|
||||
final isRateLimit = _isRateLimitError(error);
|
||||
final isTimeout = _isTimeoutError(error);
|
||||
final tag = isRateLimit
|
||||
@@ -337,6 +361,7 @@ class StreamRecoveryWrapper<T> {
|
||||
_disposed = true;
|
||||
_pollingTimer?.cancel();
|
||||
_recoveryTimer?.cancel();
|
||||
_stabilityTimer?.cancel();
|
||||
_realtimeSub?.cancel();
|
||||
// Ensure the channel is removed from the recovering set when the
|
||||
// wrapper is torn down (e.g. provider disposed during navigation).
|
||||
|
||||
@@ -526,6 +526,30 @@ List<Map<String, dynamic>> _processTasksInIsolate(
|
||||
/// Provider for task query parameters.
|
||||
final tasksQueryProvider = StateProvider<TaskQuery>((ref) => const TaskQuery());
|
||||
|
||||
/// Derived provider that selects a single [Task] by ID from the tasks list.
|
||||
///
|
||||
/// Because [Task] implements `==`, this provider only notifies watchers when
|
||||
/// the specific task's data actually changes — not when unrelated tasks in the
|
||||
/// list are updated. Use this in detail screens to avoid full-list rebuilds.
|
||||
final taskByIdProvider = Provider.family<Task?, String>((ref, taskId) {
|
||||
return ref
|
||||
.watch(tasksProvider)
|
||||
.valueOrNull
|
||||
?.where((t) => t.id == taskId)
|
||||
.firstOrNull;
|
||||
});
|
||||
|
||||
/// Derived provider that selects a [Task] linked to a given ticket ID.
|
||||
///
|
||||
/// Returns the first task whose `ticketId` matches [ticketId], or null.
|
||||
final taskByTicketIdProvider = Provider.family<Task?, String>((ref, ticketId) {
|
||||
return ref
|
||||
.watch(tasksProvider)
|
||||
.valueOrNull
|
||||
?.where((t) => t.ticketId == ticketId)
|
||||
.firstOrNull;
|
||||
});
|
||||
|
||||
final taskAssignmentsProvider = StreamProvider<List<TaskAssignment>>((ref) {
|
||||
final client = ref.watch(supabaseClientProvider);
|
||||
|
||||
|
||||
@@ -361,6 +361,19 @@ final ticketsQueryProvider = StateProvider<TicketQuery>(
|
||||
(ref) => const TicketQuery(),
|
||||
);
|
||||
|
||||
/// Derived provider that selects a single [Ticket] by ID from the tickets list.
|
||||
///
|
||||
/// Because [Ticket] implements `==`, this provider only notifies watchers when
|
||||
/// the specific ticket's data actually changes — not when unrelated tickets in
|
||||
/// the list are updated. Use this in detail screens to avoid full-list rebuilds.
|
||||
final ticketByIdProvider = Provider.family<Ticket?, String>((ref, ticketId) {
|
||||
return ref
|
||||
.watch(ticketsProvider)
|
||||
.valueOrNull
|
||||
?.where((t) => t.id == ticketId)
|
||||
.firstOrNull;
|
||||
});
|
||||
|
||||
final ticketMessagesProvider =
|
||||
StreamProvider.family<List<TicketMessage>, String>((ref, ticketId) {
|
||||
final client = ref.watch(supabaseClientProvider);
|
||||
|
||||
Reference in New Issue
Block a user