realtime controller
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
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 {
|
||||
RealtimeController(this._client) {
|
||||
_init();
|
||||
}
|
||||
|
||||
final SupabaseClient _client;
|
||||
|
||||
bool isConnecting = false;
|
||||
bool _disposed = false;
|
||||
|
||||
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 (_) {}
|
||||
}
|
||||
|
||||
/// Try to reconnect the realtime client using a small exponential backoff.
|
||||
Future<void> recoverConnection() async {
|
||||
if (_disposed) return;
|
||||
if (isConnecting) return;
|
||||
|
||||
isConnecting = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
int attempt = 0;
|
||||
int maxAttempts = 4;
|
||||
int delaySeconds = 1;
|
||||
while (attempt < maxAttempts && !_disposed) {
|
||||
attempt++;
|
||||
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));
|
||||
|
||||
// Exit early; we don't have a reliable sync API for connection
|
||||
// state across all platforms, so treat this as a best-effort
|
||||
// resurrection.
|
||||
break;
|
||||
} catch (_) {
|
||||
await Future.delayed(Duration(seconds: delaySeconds));
|
||||
delaySeconds = delaySeconds * 2;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (!_disposed) {
|
||||
isConnecting = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_disposed = true;
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user