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 '../models/notification_item.dart';
|
||||||
import '../providers/notifications_provider.dart';
|
import '../providers/notifications_provider.dart';
|
||||||
|
import '../providers/realtime_controller.dart';
|
||||||
|
|
||||||
/// Wraps the app and installs both a Supabase realtime listener and the
|
/// Wraps the app and installs both a Supabase realtime listener and the
|
||||||
/// FCM handlers described in the frontend design.
|
/// FCM handlers described in the frontend design.
|
||||||
|
|
@ -24,21 +25,35 @@ class NotificationBridge extends ConsumerStatefulWidget {
|
||||||
ConsumerState<NotificationBridge> createState() => _NotificationBridgeState();
|
ConsumerState<NotificationBridge> createState() => _NotificationBridgeState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _NotificationBridgeState extends ConsumerState<NotificationBridge> {
|
class _NotificationBridgeState extends ConsumerState<NotificationBridge>
|
||||||
|
with WidgetsBindingObserver {
|
||||||
// store previous notifications to diff
|
// store previous notifications to diff
|
||||||
List<NotificationItem> _prevList = [];
|
List<NotificationItem> _prevList = [];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
_setupFcmHandlers();
|
_setupFcmHandlers();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
super.dispose();
|
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) {
|
void _showBanner(String type, NotificationItem item) {
|
||||||
final ctx = widget.navigatorKey.currentState?.overlay?.context;
|
final ctx = widget.navigatorKey.currentState?.overlay?.context;
|
||||||
if (ctx == null) return;
|
if (ctx == null) return;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user