realtime controller
This commit is contained in:
parent
9bad41a5ee
commit
c5e859ad88
107
lib/providers/realtime_controller.dart
Normal file
107
lib/providers/realtime_controller.dart
Normal file
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import 'package:firebase_messaging/firebase_messaging.dart';
|
|||
|
||||
import '../models/notification_item.dart';
|
||||
import '../providers/notifications_provider.dart';
|
||||
import '../providers/realtime_controller.dart';
|
||||
|
||||
/// Wraps the app and installs both a Supabase realtime listener and the
|
||||
/// FCM handlers described in the frontend design.
|
||||
|
|
@ -24,21 +25,35 @@ class NotificationBridge extends ConsumerStatefulWidget {
|
|||
ConsumerState<NotificationBridge> createState() => _NotificationBridgeState();
|
||||
}
|
||||
|
||||
class _NotificationBridgeState extends ConsumerState<NotificationBridge> {
|
||||
class _NotificationBridgeState extends ConsumerState<NotificationBridge>
|
||||
with WidgetsBindingObserver {
|
||||
// store previous notifications to diff
|
||||
List<NotificationItem> _prevList = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
_setupFcmHandlers();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
super.didChangeAppLifecycleState(state);
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
try {
|
||||
// Trigger a best-effort realtime reconnection when the app resumes.
|
||||
ref.read(realtimeControllerProvider).recoverConnection();
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
void _showBanner(String type, NotificationItem item) {
|
||||
final ctx = widget.navigatorKey.currentState?.overlay?.context;
|
||||
if (ctx == null) return;
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user