130 lines
3.7 KiB
Dart
130 lines
3.7 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:supabase_flutter/supabase_flutter.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;
|
|
});
|
|
|
|
/// A lightweight controller that attempts to recover the Supabase Realtime
|
|
/// connection when the app returns to the foreground or when auth tokens
|
|
/// are refreshed.
|
|
class RealtimeController extends ChangeNotifier {
|
|
final SupabaseClient _client;
|
|
|
|
bool isConnecting = false;
|
|
bool isFailed = false;
|
|
String? lastError;
|
|
int attempts = 0;
|
|
final int maxAttempts;
|
|
bool _disposed = false;
|
|
|
|
RealtimeController(this._client, {this.maxAttempts = 4}) {
|
|
_init();
|
|
}
|
|
|
|
void _init() {
|
|
try {
|
|
// Listen for auth changes and try to recover the realtime connection
|
|
_client.auth.onAuthStateChange.listen((data) {
|
|
final event = data.event;
|
|
if (event == AuthChangeEvent.tokenRefreshed ||
|
|
event == AuthChangeEvent.signedIn) {
|
|
recoverConnection();
|
|
}
|
|
});
|
|
} catch (e) {
|
|
debugPrint('RealtimeController._init error: $e');
|
|
}
|
|
}
|
|
|
|
/// Try to reconnect the realtime client using a small exponential backoff.
|
|
Future<void> recoverConnection() async {
|
|
if (_disposed) return;
|
|
if (isConnecting) return;
|
|
|
|
isFailed = false;
|
|
lastError = null;
|
|
isConnecting = true;
|
|
notifyListeners();
|
|
|
|
try {
|
|
int delaySeconds = 1;
|
|
while (attempts < maxAttempts && !_disposed) {
|
|
attempts++;
|
|
try {
|
|
// Best-effort disconnect then connect so the realtime client picks
|
|
// up any refreshed tokens.
|
|
try {
|
|
// Try to refresh session/token if the SDK supports it. Use dynamic
|
|
// to avoid depending on a specific SDK version symbol.
|
|
try {
|
|
await (_client.auth as dynamic).refreshSession?.call();
|
|
} catch (_) {}
|
|
|
|
// Best-effort disconnect then connect so the realtime client picks
|
|
// up any refreshed tokens. The realtime connect/disconnect are
|
|
// marked internal by the SDK; suppress the lint here since this
|
|
// is a deliberate best-effort recovery.
|
|
// ignore: invalid_use_of_internal_member
|
|
_client.realtime.disconnect();
|
|
} catch (_) {}
|
|
await Future.delayed(const Duration(milliseconds: 300));
|
|
try {
|
|
// ignore: invalid_use_of_internal_member
|
|
_client.realtime.connect();
|
|
} catch (_) {}
|
|
|
|
// Give the socket a moment to stabilise.
|
|
await Future.delayed(const Duration(seconds: 1));
|
|
|
|
// Success (best-effort). Reset attempt counter and clear failure.
|
|
attempts = 0;
|
|
isFailed = false;
|
|
lastError = null;
|
|
break;
|
|
} catch (e) {
|
|
lastError = e.toString();
|
|
if (attempts >= maxAttempts) {
|
|
isFailed = true;
|
|
break;
|
|
}
|
|
await Future.delayed(Duration(seconds: delaySeconds));
|
|
delaySeconds = delaySeconds * 2;
|
|
}
|
|
}
|
|
} finally {
|
|
if (!_disposed) {
|
|
isConnecting = false;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Retry a failed recovery attempt.
|
|
Future<void> retry() async {
|
|
if (_disposed) return;
|
|
attempts = 0;
|
|
isFailed = false;
|
|
lastError = null;
|
|
notifyListeners();
|
|
await recoverConnection();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_disposed = true;
|
|
super.dispose();
|
|
}
|
|
}
|