From ceb698d0c756b6214120c3e1abe76d3e388e6ca4 Mon Sep 17 00:00:00 2001 From: Vinzent Date: Thu, 27 Mar 2025 13:58:47 +0100 Subject: [PATCH 1/8] fix: disconnect open realtime client after flutter web hot-restart --- .../lib/src/realtime_cleanup_stub.dart | 5 +++ .../lib/src/realtime_cleanup_web.dart | 35 +++++++++++++++++++ .../supabase_flutter/lib/src/supabase.dart | 5 +++ 3 files changed, 45 insertions(+) create mode 100644 packages/supabase_flutter/lib/src/realtime_cleanup_stub.dart create mode 100644 packages/supabase_flutter/lib/src/realtime_cleanup_web.dart diff --git a/packages/supabase_flutter/lib/src/realtime_cleanup_stub.dart b/packages/supabase_flutter/lib/src/realtime_cleanup_stub.dart new file mode 100644 index 00000000..9972fd5a --- /dev/null +++ b/packages/supabase_flutter/lib/src/realtime_cleanup_stub.dart @@ -0,0 +1,5 @@ +import 'package:supabase_flutter/supabase_flutter.dart'; + +void markRealtimeClientToBeDisconnected(RealtimeClient client) {} + +void disconnectPreviousRealtimeClient() {} diff --git a/packages/supabase_flutter/lib/src/realtime_cleanup_web.dart b/packages/supabase_flutter/lib/src/realtime_cleanup_web.dart new file mode 100644 index 00000000..114e818e --- /dev/null +++ b/packages/supabase_flutter/lib/src/realtime_cleanup_web.dart @@ -0,0 +1,35 @@ +import 'dart:js_interop'; + +import 'package:supabase_flutter/supabase_flutter.dart'; + +@JS() +external JSFunction? supabaseFlutterWSToClose; + +/// Store a function to properly disconnect the previous [RealtimeClient] in +/// the js context. +/// +/// WebSocket connections are not closed when Flutter is hot-restarted on web. +/// +/// This causes old dart code that is still associated with the WebSocket +/// connection to be still running and causes unexpected behavior like type +/// errors and the fact that the events to the old connection may still be +/// logged. +void markRealtimeClientToBeDisconnected(RealtimeClient client) { + void disconnect() { + client.disconnect( + code: 1000, reason: 'Closed due to Flutter Web hot-restart'); + } + + supabaseFlutterWSToClose = disconnect.toJS; +} + +/// Disconnect the previous [RealtimeClient] if it exists. +/// +/// This is done by calling the function stored by +/// [markRealtimeClientToBeDisconnected] from the js context +void disconnectPreviousRealtimeClient() { + if (supabaseFlutterWSToClose != null) { + supabaseFlutterWSToClose!.callAsFunction(); + supabaseFlutterWSToClose = null; + } +} diff --git a/packages/supabase_flutter/lib/src/supabase.dart b/packages/supabase_flutter/lib/src/supabase.dart index 45a7cc29..b10f3c0c 100644 --- a/packages/supabase_flutter/lib/src/supabase.dart +++ b/packages/supabase_flutter/lib/src/supabase.dart @@ -11,6 +11,9 @@ import 'package:supabase_flutter/src/flutter_go_true_client_options.dart'; import 'package:supabase_flutter/src/local_storage.dart'; import 'package:supabase_flutter/src/supabase_auth.dart'; +import 'realtime_cleanup_stub.dart' + if (dart.library.js_interop) 'realtime_cleanup_web.dart'; + import 'version.dart'; final _log = Logger('supabase.supabase_flutter'); @@ -203,6 +206,8 @@ class Supabase with WidgetsBindingObserver { authOptions: authOptions, accessToken: accessToken, ); + disconnectPreviousRealtimeClient(); + markRealtimeClientToBeDisconnected(client.realtime); _widgetsBindingInstance?.addObserver(this); _initialized = true; } From 1ccfd7fafe36c5c8793d41887ac81f3f5f7e8dda Mon Sep 17 00:00:00 2001 From: Vinzent Date: Thu, 27 Mar 2025 14:07:38 +0100 Subject: [PATCH 2/8] refactor: remove unused longpollerTimeout --- packages/realtime_client/lib/src/realtime_client.dart | 5 ++--- packages/supabase_flutter/lib/src/realtime_cleanup_web.dart | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/realtime_client/lib/src/realtime_client.dart b/packages/realtime_client/lib/src/realtime_client.dart index dece4760..26c87c40 100644 --- a/packages/realtime_client/lib/src/realtime_client.dart +++ b/packages/realtime_client/lib/src/realtime_client.dart @@ -88,6 +88,8 @@ class RealtimeClient { 'error': [], 'message': [] }; + + @Deprecated("No longer used. Will be removed in the next major version.") int longpollerTimeout = 20000; SocketStates? connState; // This is called `accessToken` in realtime-js @@ -113,8 +115,6 @@ class RealtimeClient { /// /// [decode] The function to decode incoming messages. Defaults to JSON: (payload, callback) => callback(JSON.parse(payload)) /// - /// [longpollerTimeout] The maximum timeout of a long poll AJAX request. Defaults to 20s (double the server long poll timer). - /// /// [reconnectAfterMs] The optional function that returns the millsec reconnect interval. Defaults to stepped backoff off. /// /// [logLevel] Specifies the log level for the connection on the server. @@ -198,7 +198,6 @@ class RealtimeClient { connState = SocketStates.open; _onConnOpen(); - conn!.stream.timeout(Duration(milliseconds: longpollerTimeout)); conn!.stream.listen( // incoming messages (message) => onConnMessage(message as String), diff --git a/packages/supabase_flutter/lib/src/realtime_cleanup_web.dart b/packages/supabase_flutter/lib/src/realtime_cleanup_web.dart index 114e818e..5036fb37 100644 --- a/packages/supabase_flutter/lib/src/realtime_cleanup_web.dart +++ b/packages/supabase_flutter/lib/src/realtime_cleanup_web.dart @@ -12,7 +12,7 @@ external JSFunction? supabaseFlutterWSToClose; /// /// This causes old dart code that is still associated with the WebSocket /// connection to be still running and causes unexpected behavior like type -/// errors and the fact that the events to the old connection may still be +/// errors and the fact that the events of the old connection may still be /// logged. void markRealtimeClientToBeDisconnected(RealtimeClient client) { void disconnect() { From 9f3066b67ca4443e37a27247549484876a7a9c0e Mon Sep 17 00:00:00 2001 From: Vinzent Date: Thu, 27 Mar 2025 14:11:12 +0100 Subject: [PATCH 3/8] refactor: remove usage of longpollerTimeout --- packages/realtime_client/lib/src/realtime_client.dart | 2 +- packages/realtime_client/test/socket_test.dart | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/realtime_client/lib/src/realtime_client.dart b/packages/realtime_client/lib/src/realtime_client.dart index 26c87c40..ff3ba10f 100644 --- a/packages/realtime_client/lib/src/realtime_client.dart +++ b/packages/realtime_client/lib/src/realtime_client.dart @@ -145,7 +145,7 @@ class RealtimeClient { }, transport = transport ?? createWebSocketClient { _log.config( - 'Initialize RealtimeClient with endpoint: $endPoint, timeout: $timeout, heartbeatIntervalMs: $heartbeatIntervalMs, longpollerTimeout: $longpollerTimeout, logLevel: $logLevel'); + 'Initialize RealtimeClient with endpoint: $endPoint, timeout: $timeout, heartbeatIntervalMs: $heartbeatIntervalMs, logLevel: $logLevel'); _log.finest('Initialize with headers: $headers, params: $params'); final customJWT = this.headers['Authorization']?.split(' ').last; accessToken = customJWT ?? params['apikey']; diff --git a/packages/realtime_client/test/socket_test.dart b/packages/realtime_client/test/socket_test.dart index 16951192..793b158d 100644 --- a/packages/realtime_client/test/socket_test.dart +++ b/packages/realtime_client/test/socket_test.dart @@ -78,7 +78,6 @@ void main() { 'message': [], }); expect(socket.timeout, const Duration(milliseconds: 10000)); - expect(socket.longpollerTimeout, 20000); expect(socket.heartbeatIntervalMs, Constants.defaultHeartbeatIntervalMs); expect( socket.logger is void Function( @@ -99,7 +98,6 @@ void main() { final socket = RealtimeClient( 'wss://example.com/socket', timeout: const Duration(milliseconds: 40000), - longpollerTimeout: 50000, heartbeatIntervalMs: 60000, // ignore: avoid_print logger: (kind, msg, data) => print('[$kind] $msg $data'), @@ -116,7 +114,6 @@ void main() { 'message': [], }); expect(socket.timeout, const Duration(milliseconds: 40000)); - expect(socket.longpollerTimeout, 50000); expect(socket.heartbeatIntervalMs, 60000); expect( socket.logger is void Function( From d26e1d5e7c17bee8dfa2ad78988abfea1f58d227 Mon Sep 17 00:00:00 2001 From: Vinzent Date: Fri, 28 Mar 2025 13:41:52 +0100 Subject: [PATCH 4/8] refactor: only apply manual disconnect in debug mode --- packages/supabase_flutter/lib/src/supabase.dart | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/supabase_flutter/lib/src/supabase.dart b/packages/supabase_flutter/lib/src/supabase.dart index b10f3c0c..47ae5d3d 100644 --- a/packages/supabase_flutter/lib/src/supabase.dart +++ b/packages/supabase_flutter/lib/src/supabase.dart @@ -206,8 +206,13 @@ class Supabase with WidgetsBindingObserver { authOptions: authOptions, accessToken: accessToken, ); - disconnectPreviousRealtimeClient(); - markRealtimeClientToBeDisconnected(client.realtime); + + // Close any previous realtime client that may still be connected due to + // flutter web hot-restart. + if (kDebugMode) { + disconnectPreviousRealtimeClient(); + markRealtimeClientToBeDisconnected(client.realtime); + } _widgetsBindingInstance?.addObserver(this); _initialized = true; } From b8cf5bc1739d0c2378552400f451b892fc70b925 Mon Sep 17 00:00:00 2001 From: Vinzent Date: Mon, 31 Mar 2025 15:36:38 +0200 Subject: [PATCH 5/8] refactor: dispose whole supabase client on hot-restart --- packages/supabase/lib/src/supabase_client.dart | 1 + ...up_stub.dart => hot_restart_cleanup_stub.dart} | 2 +- ...anup_web.dart => hot_restart_cleanup_web.dart} | 15 ++++++++------- packages/supabase_flutter/lib/src/supabase.dart | 6 +++--- 4 files changed, 13 insertions(+), 11 deletions(-) rename packages/supabase_flutter/lib/src/{realtime_cleanup_stub.dart => hot_restart_cleanup_stub.dart} (61%) rename packages/supabase_flutter/lib/src/{realtime_cleanup_web.dart => hot_restart_cleanup_web.dart} (58%) diff --git a/packages/supabase/lib/src/supabase_client.dart b/packages/supabase/lib/src/supabase_client.dart index 8f095238..382569f8 100644 --- a/packages/supabase/lib/src/supabase_client.dart +++ b/packages/supabase/lib/src/supabase_client.dart @@ -268,6 +268,7 @@ class SupabaseClient { Future dispose() async { _log.fine('Dispose SupabaseClient'); + await realtime.disconnect(); await _authStateSubscription?.cancel(); await _isolate.dispose(); _authInstance?.dispose(); diff --git a/packages/supabase_flutter/lib/src/realtime_cleanup_stub.dart b/packages/supabase_flutter/lib/src/hot_restart_cleanup_stub.dart similarity index 61% rename from packages/supabase_flutter/lib/src/realtime_cleanup_stub.dart rename to packages/supabase_flutter/lib/src/hot_restart_cleanup_stub.dart index 9972fd5a..9389c0b3 100644 --- a/packages/supabase_flutter/lib/src/realtime_cleanup_stub.dart +++ b/packages/supabase_flutter/lib/src/hot_restart_cleanup_stub.dart @@ -1,5 +1,5 @@ import 'package:supabase_flutter/supabase_flutter.dart'; -void markRealtimeClientToBeDisconnected(RealtimeClient client) {} +void markRealtimeClientToBeDisconnected(SupabaseClient client) {} void disconnectPreviousRealtimeClient() {} diff --git a/packages/supabase_flutter/lib/src/realtime_cleanup_web.dart b/packages/supabase_flutter/lib/src/hot_restart_cleanup_web.dart similarity index 58% rename from packages/supabase_flutter/lib/src/realtime_cleanup_web.dart rename to packages/supabase_flutter/lib/src/hot_restart_cleanup_web.dart index 5036fb37..57a4fba0 100644 --- a/packages/supabase_flutter/lib/src/realtime_cleanup_web.dart +++ b/packages/supabase_flutter/lib/src/hot_restart_cleanup_web.dart @@ -5,25 +5,26 @@ import 'package:supabase_flutter/supabase_flutter.dart'; @JS() external JSFunction? supabaseFlutterWSToClose; -/// Store a function to properly disconnect the previous [RealtimeClient] in +/// Store a function to properly dispose the previous [SupabaseClient] in /// the js context. /// -/// WebSocket connections are not closed when Flutter is hot-restarted on web. +/// WebSocket connections and [BroadcastChannel] are not closed when Flutter is hot-restarted on web. /// -/// This causes old dart code that is still associated with the WebSocket -/// connection to be still running and causes unexpected behavior like type +/// This causes old dart code that is still associated with those +/// connections to be still running and causes unexpected behavior like type /// errors and the fact that the events of the old connection may still be /// logged. -void markRealtimeClientToBeDisconnected(RealtimeClient client) { +void markRealtimeClientToBeDisconnected(SupabaseClient client) { void disconnect() { - client.disconnect( + client.realtime.disconnect( code: 1000, reason: 'Closed due to Flutter Web hot-restart'); + client.dispose(); } supabaseFlutterWSToClose = disconnect.toJS; } -/// Disconnect the previous [RealtimeClient] if it exists. +/// Disconnect the previous [SupabaseClient] if it exists. /// /// This is done by calling the function stored by /// [markRealtimeClientToBeDisconnected] from the js context diff --git a/packages/supabase_flutter/lib/src/supabase.dart b/packages/supabase_flutter/lib/src/supabase.dart index 47ae5d3d..f1159c40 100644 --- a/packages/supabase_flutter/lib/src/supabase.dart +++ b/packages/supabase_flutter/lib/src/supabase.dart @@ -11,8 +11,8 @@ import 'package:supabase_flutter/src/flutter_go_true_client_options.dart'; import 'package:supabase_flutter/src/local_storage.dart'; import 'package:supabase_flutter/src/supabase_auth.dart'; -import 'realtime_cleanup_stub.dart' - if (dart.library.js_interop) 'realtime_cleanup_web.dart'; +import 'hot_restart_cleanup_stub.dart' + if (dart.library.js_interop) 'hot_restart_cleanup_web.dart'; import 'version.dart'; @@ -211,7 +211,7 @@ class Supabase with WidgetsBindingObserver { // flutter web hot-restart. if (kDebugMode) { disconnectPreviousRealtimeClient(); - markRealtimeClientToBeDisconnected(client.realtime); + markRealtimeClientToBeDisconnected(client); } _widgetsBindingInstance?.addObserver(this); _initialized = true; From 08ce68c37a014cb12d1e6e034b73d11c7b900c42 Mon Sep 17 00:00:00 2001 From: Vinzent Date: Mon, 31 Mar 2025 15:37:58 +0200 Subject: [PATCH 6/8] fix: properly migrate broadcast channel to web package --- packages/gotrue/lib/src/broadcast_web.dart | 17 ++++++++--------- packages/gotrue/lib/src/gotrue_client.dart | 3 ++- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/gotrue/lib/src/broadcast_web.dart b/packages/gotrue/lib/src/broadcast_web.dart index 72ba708f..8f50c04c 100644 --- a/packages/gotrue/lib/src/broadcast_web.dart +++ b/packages/gotrue/lib/src/broadcast_web.dart @@ -12,15 +12,14 @@ BroadcastChannel getBroadcastChannel(String broadcastKey) { final broadcast = web.BroadcastChannel(broadcastKey); final controller = StreamController>(); - broadcast.addEventListener( - 'message', - (web.Event event) { - if (event is web.MessageEvent) { - final dataMap = event.data.dartify(); - controller.add(json.decode(json.encode(dataMap))); - } - } as web.EventListener, - ); + void onMessage(web.Event event) { + if (event is web.MessageEvent) { + final dataMap = event.data.dartify(); + controller.add(json.decode(json.encode(dataMap))); + } + } + + broadcast.onmessage = onMessage.toJS; return ( onMessage: controller.stream, diff --git a/packages/gotrue/lib/src/gotrue_client.dart b/packages/gotrue/lib/src/gotrue_client.dart index 96e020b3..965f6149 100644 --- a/packages/gotrue/lib/src/gotrue_client.dart +++ b/packages/gotrue/lib/src/gotrue_client.dart @@ -1205,7 +1205,8 @@ class GoTrueClient { notifyAllSubscribers(event, session: session, broadcast: false); } }); - } catch (e) { + } catch (error, stackTrace) { + _log.warning('Failed to start broadcast channel', error, stackTrace); // Ignoring } } From 0bfe534aa03003ba8c0f8c6cb368a53a832e2da9 Mon Sep 17 00:00:00 2001 From: Vinzent Date: Mon, 31 Mar 2025 15:58:28 +0200 Subject: [PATCH 7/8] refactor: rename methods --- .../supabase_flutter/lib/src/hot_restart_cleanup_stub.dart | 4 ++-- .../supabase_flutter/lib/src/hot_restart_cleanup_web.dart | 6 +++--- packages/supabase_flutter/lib/src/supabase.dart | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/supabase_flutter/lib/src/hot_restart_cleanup_stub.dart b/packages/supabase_flutter/lib/src/hot_restart_cleanup_stub.dart index 9389c0b3..ff87e681 100644 --- a/packages/supabase_flutter/lib/src/hot_restart_cleanup_stub.dart +++ b/packages/supabase_flutter/lib/src/hot_restart_cleanup_stub.dart @@ -1,5 +1,5 @@ import 'package:supabase_flutter/supabase_flutter.dart'; -void markRealtimeClientToBeDisconnected(SupabaseClient client) {} +void markClientToDispose(SupabaseClient client) {} -void disconnectPreviousRealtimeClient() {} +void disposePreviousClient() {} diff --git a/packages/supabase_flutter/lib/src/hot_restart_cleanup_web.dart b/packages/supabase_flutter/lib/src/hot_restart_cleanup_web.dart index 57a4fba0..3324a0ae 100644 --- a/packages/supabase_flutter/lib/src/hot_restart_cleanup_web.dart +++ b/packages/supabase_flutter/lib/src/hot_restart_cleanup_web.dart @@ -14,7 +14,7 @@ external JSFunction? supabaseFlutterWSToClose; /// connections to be still running and causes unexpected behavior like type /// errors and the fact that the events of the old connection may still be /// logged. -void markRealtimeClientToBeDisconnected(SupabaseClient client) { +void markClientToDispose(SupabaseClient client) { void disconnect() { client.realtime.disconnect( code: 1000, reason: 'Closed due to Flutter Web hot-restart'); @@ -27,8 +27,8 @@ void markRealtimeClientToBeDisconnected(SupabaseClient client) { /// Disconnect the previous [SupabaseClient] if it exists. /// /// This is done by calling the function stored by -/// [markRealtimeClientToBeDisconnected] from the js context -void disconnectPreviousRealtimeClient() { +/// [markClientToDispose] from the js context +void disposePreviousClient() { if (supabaseFlutterWSToClose != null) { supabaseFlutterWSToClose!.callAsFunction(); supabaseFlutterWSToClose = null; diff --git a/packages/supabase_flutter/lib/src/supabase.dart b/packages/supabase_flutter/lib/src/supabase.dart index f1159c40..8ead5e17 100644 --- a/packages/supabase_flutter/lib/src/supabase.dart +++ b/packages/supabase_flutter/lib/src/supabase.dart @@ -210,8 +210,8 @@ class Supabase with WidgetsBindingObserver { // Close any previous realtime client that may still be connected due to // flutter web hot-restart. if (kDebugMode) { - disconnectPreviousRealtimeClient(); - markRealtimeClientToBeDisconnected(client); + disposePreviousClient(); + markClientToDispose(client); } _widgetsBindingInstance?.addObserver(this); _initialized = true; From a1be4e4bfb034b2910afb6c3c3812435e66f8ba2 Mon Sep 17 00:00:00 2001 From: Vinzent Date: Mon, 31 Mar 2025 15:59:28 +0200 Subject: [PATCH 8/8] refactor: rename external js field --- .../lib/src/hot_restart_cleanup_web.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/supabase_flutter/lib/src/hot_restart_cleanup_web.dart b/packages/supabase_flutter/lib/src/hot_restart_cleanup_web.dart index 3324a0ae..fcc702ee 100644 --- a/packages/supabase_flutter/lib/src/hot_restart_cleanup_web.dart +++ b/packages/supabase_flutter/lib/src/hot_restart_cleanup_web.dart @@ -3,7 +3,7 @@ import 'dart:js_interop'; import 'package:supabase_flutter/supabase_flutter.dart'; @JS() -external JSFunction? supabaseFlutterWSToClose; +external JSFunction? supabaseFlutterClientToDispose; /// Store a function to properly dispose the previous [SupabaseClient] in /// the js context. @@ -15,13 +15,13 @@ external JSFunction? supabaseFlutterWSToClose; /// errors and the fact that the events of the old connection may still be /// logged. void markClientToDispose(SupabaseClient client) { - void disconnect() { + void dispose() { client.realtime.disconnect( code: 1000, reason: 'Closed due to Flutter Web hot-restart'); client.dispose(); } - supabaseFlutterWSToClose = disconnect.toJS; + supabaseFlutterClientToDispose = dispose.toJS; } /// Disconnect the previous [SupabaseClient] if it exists. @@ -29,8 +29,8 @@ void markClientToDispose(SupabaseClient client) { /// This is done by calling the function stored by /// [markClientToDispose] from the js context void disposePreviousClient() { - if (supabaseFlutterWSToClose != null) { - supabaseFlutterWSToClose!.callAsFunction(); - supabaseFlutterWSToClose = null; + if (supabaseFlutterClientToDispose != null) { + supabaseFlutterClientToDispose!.callAsFunction(); + supabaseFlutterClientToDispose = null; } }