Files
tasq/lib/providers/realtime_controller.dart
T
2026-04-27 06:20:39 +08:00

100 lines
3.8 KiB
Dart

import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'stream_recovery.dart';
final realtimeControllerProvider = ChangeNotifierProvider<RealtimeController>((
ref,
) {
// ChangeNotifierProvider automatically disposes the notifier; no need
// for ref.onDispose here — adding it causes a double-dispose assertion.
return RealtimeController();
});
/// Per-channel realtime controller for UI skeleton indicators.
///
/// Individual streams handle their own recovery via [StreamRecoveryWrapper].
/// This controller aggregates channel-level status so the UI can show
/// per-channel skeleton shimmers (e.g. only the tasks list shimmers when
/// the `tasks` channel is recovering, not the whole app).
///
/// Coordinates:
/// - Per-channel recovering state for pinpoint skeleton indicators
/// - Auth token refreshes for realtime connections
class RealtimeController extends ChangeNotifier {
bool _disposed = false;
/// Channels currently in a recovering/polling/stale state.
final Set<String> _recoveringChannels = {};
RealtimeController();
// ── Per-channel status ─────────────────────────────────────────────────
/// Whether a specific channel is currently recovering.
bool isChannelRecovering(String channel) =>
_recoveringChannels.contains(channel);
/// Global flag: true if **any** channel is recovering. Useful for global
/// indicators (e.g. dashboard) where per-channel granularity isn't needed.
bool get isAnyStreamRecovering => _recoveringChannels.isNotEmpty;
/// The set of channels currently recovering, for UI display.
Set<String> get recoveringChannels => Set.unmodifiable(_recoveringChannels);
/// Mark a channel as recovering. Called by [StreamRecoveryWrapper] via its
/// [ChannelStatusCallback].
void markChannelRecovering(String channel) {
if (_disposed) return;
if (_recoveringChannels.add(channel)) {
notifyListeners();
}
}
/// Mark a channel as recovered. Called when realtime reconnects
/// successfully.
void markChannelRecovered(String channel) {
if (_disposed) return;
if (_recoveringChannels.remove(channel)) {
notifyListeners();
}
}
/// Convenience callback suitable for [StreamRecoveryWrapper.onStatusChanged].
///
/// Routes [StreamConnectionStatus] to the appropriate mark method:
/// - `connected` — live stream; no indicator needed.
/// - `polling` — REST fallback; data is fresh, no skeleton needed.
/// - `offline` — device has no internet; Brick cache is active. The offline
/// banner already communicates this state, so the reconnection skeleton
/// should NOT show — it would be misleading while real data is on screen.
/// - Everything else (`recovering`, `stale`, `failed`) — show skeleton.
void handleChannelStatus(String channel, StreamConnectionStatus status) {
if (status == StreamConnectionStatus.connected ||
status == StreamConnectionStatus.polling ||
status == StreamConnectionStatus.offline) {
markChannelRecovered(channel);
} else {
markChannelRecovering(channel);
}
}
// ── Legacy compat ─────────────────────────────────────────────────────
/// @deprecated Use [markChannelRecovering] instead.
void markStreamRecovering() {
// Kept for backward compatibility; maps to a synthetic channel.
markChannelRecovering('_global');
}
/// @deprecated Use [markChannelRecovered] instead.
void markStreamRecovered() {
markChannelRecovered('_global');
}
@override
void dispose() {
_disposed = true;
super.dispose();
}
}