124 lines
3.9 KiB
Dart
124 lines
3.9 KiB
Dart
import 'dart:async';
|
|
|
|
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>((
|
|
ref,
|
|
) {
|
|
final client = ref.watch(supabaseClientProvider);
|
|
final controller = RealtimeController(client);
|
|
ref.onDispose(controller.dispose);
|
|
return controller;
|
|
});
|
|
|
|
/// 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;
|
|
|
|
/// Channels currently in a recovering/polling/stale state.
|
|
final Set<String> _recoveringChannels = {};
|
|
|
|
RealtimeController(this._client) {
|
|
_init();
|
|
}
|
|
|
|
void _init() {
|
|
try {
|
|
_client.auth.onAuthStateChange.listen((data) {
|
|
final event = data.event;
|
|
if (event == AuthChangeEvent.tokenRefreshed) {
|
|
_ensureTokenFresh();
|
|
}
|
|
});
|
|
} catch (e) {
|
|
debugPrint('RealtimeController._init error: $e');
|
|
}
|
|
}
|
|
|
|
Future<void> _ensureTokenFresh() async {
|
|
if (_disposed) return;
|
|
try {
|
|
final authDynamic = _client.auth as dynamic;
|
|
if (authDynamic.refreshSession != null) {
|
|
await authDynamic.refreshSession?.call();
|
|
}
|
|
} catch (e) {
|
|
debugPrint('RealtimeController: token refresh failed: $e');
|
|
}
|
|
}
|
|
|
|
// ── 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();
|
|
}
|
|
}
|
|
|
|
/// 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;
|
|
super.dispose();
|
|
}
|
|
}
|