Per channel skeleton
This commit is contained in:
@@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
import 'stream_recovery.dart';
|
||||
import 'supabase_provider.dart';
|
||||
|
||||
final realtimeControllerProvider = ChangeNotifierProvider<RealtimeController>((
|
||||
@@ -15,18 +16,22 @@ final realtimeControllerProvider = ChangeNotifierProvider<RealtimeController>((
|
||||
return controller;
|
||||
});
|
||||
|
||||
/// Simplified realtime controller for app-lifecycle awareness.
|
||||
/// Individual streams now handle their own recovery via [StreamRecoveryWrapper].
|
||||
/// This controller only coordinates:
|
||||
/// - App lifecycle transitions (background/foreground)
|
||||
/// - Auth token refreshes
|
||||
/// - Global connection state notification (for UI indicators)
|
||||
/// 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 {
|
||||
final SupabaseClient _client;
|
||||
bool _disposed = false;
|
||||
|
||||
/// Global flag: true if any stream is recovering; used for subtle UI indicator.
|
||||
bool isAnyStreamRecovering = false;
|
||||
/// Channels currently in a recovering/polling/stale state.
|
||||
final Set<String> _recoveringChannels = {};
|
||||
|
||||
RealtimeController(this._client) {
|
||||
_init();
|
||||
@@ -34,12 +39,8 @@ class RealtimeController extends ChangeNotifier {
|
||||
|
||||
void _init() {
|
||||
try {
|
||||
// Listen for auth changes; ensure tokens are fresh for realtime.
|
||||
// Individual streams will handle their own reconnection.
|
||||
_client.auth.onAuthStateChange.listen((data) {
|
||||
final event = data.event;
|
||||
// Only refresh token on existing session refreshes, not immediately after sign-in
|
||||
// (sign-in already provides a fresh token)
|
||||
if (event == AuthChangeEvent.tokenRefreshed) {
|
||||
_ensureTokenFresh();
|
||||
}
|
||||
@@ -49,12 +50,9 @@ class RealtimeController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensure auth token is fresh for upcoming realtime operations.
|
||||
/// This is called after token refresh events, not immediately after sign-in.
|
||||
Future<void> _ensureTokenFresh() async {
|
||||
if (_disposed) return;
|
||||
try {
|
||||
// Defensive: only refresh if the method exists (SDK version compatibility)
|
||||
final authDynamic = _client.auth as dynamic;
|
||||
if (authDynamic.refreshSession != null) {
|
||||
await authDynamic.refreshSession?.call();
|
||||
@@ -64,24 +62,59 @@ class RealtimeController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// Notify that a stream is starting recovery. Used for global UI indicator.
|
||||
void markStreamRecovering() {
|
||||
if (!isAnyStreamRecovering) {
|
||||
isAnyStreamRecovering = true;
|
||||
// ── 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 (_recoveringChannels.add(channel)) {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// Notify that stream recovery completed. If all streams recovered, update state.
|
||||
void markStreamRecovered() {
|
||||
// In practice, individual streams notify their own status via statusChanges.
|
||||
// This is kept for potential future global coordination.
|
||||
if (isAnyStreamRecovering) {
|
||||
isAnyStreamRecovering = false;
|
||||
/// Mark a channel as recovered. Called when realtime reconnects
|
||||
/// successfully.
|
||||
void markChannelRecovered(String channel) {
|
||||
if (_recoveringChannels.remove(channel)) {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience callback suitable for [StreamRecoveryWrapper.onStatusChanged].
|
||||
///
|
||||
/// Routes [StreamConnectionStatus] to the appropriate mark method.
|
||||
void handleChannelStatus(String channel, StreamConnectionStatus status) {
|
||||
if (status == StreamConnectionStatus.connected) {
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user