@@ -13,7 +13,6 @@ import 'package:supabase_flutter/src/supabase_auth.dart';
1313
1414import 'hot_restart_cleanup_stub.dart'
1515 if (dart.library.js_interop) 'hot_restart_cleanup_web.dart' ;
16-
1716import 'version.dart' ;
1817
1918final _log = Logger ('supabase.supabase_flutter' );
@@ -169,14 +168,20 @@ class Supabase with WidgetsBindingObserver {
169168 /// Wraps the `recoverSession()` call so that it can be terminated when `dispose()` is called
170169 late CancelableOperation _restoreSessionCancellableOperation;
171170
172- CancelableOperation <void >? _realtimeReconnectOperation;
171+ /// Serial queue for lifecycle operations (connect/disconnect). Each event
172+ /// appends via `.then()` so operations never overlap.
173+ Future <void > _pendingLifecycleOperation = Future .value ();
173174
174- Future <void >? _disconnectFuture;
175+ /// The most recently requested lifecycle state. Checked inside
176+ /// [_processLifecycle] after each `await` to skip stale operations
177+ /// (e.g. abort a reconnect if the app went back to background).
178+ AppLifecycleState ? _targetLifecycleState;
175179
176180 StreamSubscription ? _logSubscription;
177181
178182 /// Dispose the instance to free up resources.
179183 Future <void > dispose () async {
184+ _targetLifecycleState = null ;
180185 await _restoreSessionCancellableOperation.cancel ();
181186 _logSubscription? .cancel ();
182187 client.dispose ();
@@ -226,80 +231,59 @@ class Supabase with WidgetsBindingObserver {
226231 void didChangeAppLifecycleState (AppLifecycleState state) {
227232 switch (state) {
228233 case AppLifecycleState .resumed:
229- onResumed ();
230- case AppLifecycleState .detached:
231234 case AppLifecycleState .paused:
232- _realtimeReconnectOperation? .cancel ();
233- _disconnectFuture = Supabase .instance.client.realtime.disconnect ();
235+ case AppLifecycleState .detached:
236+ _targetLifecycleState = state;
237+ _pendingLifecycleOperation = _pendingLifecycleOperation
238+ .then ((_) => _processLifecycle (state))
239+ .catchError ((_) {});
234240 default :
241+ break ;
235242 }
236243 }
237244
238- Future <void > onResumed () async {
245+ /// Processes a lifecycle state change. Operations are serialized via
246+ /// [_pendingLifecycleOp] so that disconnect and connect never overlap.
247+ ///
248+ /// [captured] is the lifecycle state at the time the event was enqueued.
249+ /// If a newer event has arrived since, this one is skipped (stale).
250+ Future <void > _processLifecycle (AppLifecycleState captured) async {
251+ // Skip if a newer lifecycle event has superseded this one.
252+ if (captured != _targetLifecycleState) return ;
253+
239254 final realtime = Supabase .instance.client.realtime;
240- if (realtime.channels.isNotEmpty) {
241- final disconnectFuture = _disconnectFuture;
242- if (disconnectFuture != null ) {
243- // If a disconnect is still in progress from e.g.
244- // [AppLifecycleState.paused] we should wait for it to finish before
245- // reconnecting. This avoids accessing conn! which may be nullified.
246- //
247- // We clear _disconnectFuture only after the future completes (not
248- // eagerly) so that a second resumed event (e.g. inactive → resumed)
249- // still sees the in-progress disconnect and waits for it instead of
250- // falling through to the else branch where connect() would no-op.
251-
252- bool cancel = false ;
253- final connectFuture = disconnectFuture.then ((_) async {
254- // Only clear if not replaced by a newer disconnect from a
255- // subsequent paused event.
256- if (_disconnectFuture == disconnectFuture) {
257- _disconnectFuture = null ;
258- }
259-
260- // Make this connect cancelable so that it does not connect if the
261- // disconnect took so long that the app is already in background
262- // again.
263- if (! cancel) {
264- // ignore: invalid_use_of_internal_member
265- await realtime.connect ();
266- for (final channel in realtime.channels) {
267- // ignore: invalid_use_of_internal_member
268- if (channel.isJoined) {
269- // ignore: invalid_use_of_internal_member
270- channel.forceRejoin ();
271- }
272- }
273- }
274- }, onError: (error) {
275- if (_disconnectFuture == disconnectFuture) {
276- _disconnectFuture = null ;
277- }
278- });
279- _realtimeReconnectOperation = CancelableOperation .fromFuture (
280- connectFuture,
281- onCancel: () => cancel = true ,
282- );
283- } else if (! realtime.isConnected) {
284- // Reconnect if the socket is currently not connected.
285- // When coming from [AppLifecycleState.paused] this should be the case,
286- // but when coming from [AppLifecycleState.inactive] no disconnect
287- // happened and therefore connection should still be intact and we
288- // should not reconnect.
289255
290- // ignore: invalid_use_of_internal_member
291- await realtime.connect ();
292- for (final channel in realtime.channels) {
293- // Only rejoin channels that think they are still joined and not
294- // which were manually unsubscribed by the user while in background
256+ if (captured == AppLifecycleState .resumed) {
257+ // No channels subscribed — nothing to reconnect.
258+ if (realtime.channels.isEmpty) return ;
295259
260+ // Already connected (e.g. coming from [AppLifecycleState.inactive]
261+ // where no disconnect happened).
262+ if (realtime.isConnected) return ;
263+
264+ // ignore: invalid_use_of_internal_member
265+ await realtime.connect ();
266+
267+ // Abort rejoin if app went back to background during connect.
268+ if (_targetLifecycleState != AppLifecycleState .resumed) return ;
269+
270+ // Re-send join messages for channels that were previously joined.
271+ // After a disconnect/reconnect the WebSocket is fresh, but the
272+ // channel objects still have joined state — forceRejoin() restores
273+ // the server-side subscriptions.
274+ for (final channel in realtime.channels) {
275+ // ignore: invalid_use_of_internal_member
276+ if (channel.isJoined) {
296277 // ignore: invalid_use_of_internal_member
297- if (channel.isJoined) {
298- // ignore: invalid_use_of_internal_member
299- channel.forceRejoin ();
300- }
278+ channel.forceRejoin ();
301279 }
302280 }
281+ } else {
282+ // paused or detached — disconnect the WebSocket if it is active.
283+ if (realtime.isConnected ||
284+ realtime.connState == SocketStates .connecting) {
285+ await realtime.disconnect ();
286+ }
303287 }
304288 }
305289}
0 commit comments