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 } } diff --git a/packages/realtime_client/lib/src/realtime_client.dart b/packages/realtime_client/lib/src/realtime_client.dart index dece4760..ff3ba10f 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. @@ -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']; @@ -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/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( 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/hot_restart_cleanup_stub.dart b/packages/supabase_flutter/lib/src/hot_restart_cleanup_stub.dart new file mode 100644 index 00000000..ff87e681 --- /dev/null +++ b/packages/supabase_flutter/lib/src/hot_restart_cleanup_stub.dart @@ -0,0 +1,5 @@ +import 'package:supabase_flutter/supabase_flutter.dart'; + +void markClientToDispose(SupabaseClient client) {} + +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 new file mode 100644 index 00000000..fcc702ee --- /dev/null +++ b/packages/supabase_flutter/lib/src/hot_restart_cleanup_web.dart @@ -0,0 +1,36 @@ +import 'dart:js_interop'; + +import 'package:supabase_flutter/supabase_flutter.dart'; + +@JS() +external JSFunction? supabaseFlutterClientToDispose; + +/// Store a function to properly dispose the previous [SupabaseClient] in +/// the js context. +/// +/// WebSocket connections and [BroadcastChannel] are not closed when Flutter is hot-restarted on web. +/// +/// 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 markClientToDispose(SupabaseClient client) { + void dispose() { + client.realtime.disconnect( + code: 1000, reason: 'Closed due to Flutter Web hot-restart'); + client.dispose(); + } + + supabaseFlutterClientToDispose = dispose.toJS; +} + +/// Disconnect the previous [SupabaseClient] if it exists. +/// +/// This is done by calling the function stored by +/// [markClientToDispose] from the js context +void disposePreviousClient() { + if (supabaseFlutterClientToDispose != null) { + supabaseFlutterClientToDispose!.callAsFunction(); + supabaseFlutterClientToDispose = null; + } +} diff --git a/packages/supabase_flutter/lib/src/supabase.dart b/packages/supabase_flutter/lib/src/supabase.dart index 45a7cc29..8ead5e17 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 'hot_restart_cleanup_stub.dart' + if (dart.library.js_interop) 'hot_restart_cleanup_web.dart'; + import 'version.dart'; final _log = Logger('supabase.supabase_flutter'); @@ -203,6 +206,13 @@ class Supabase with WidgetsBindingObserver { authOptions: authOptions, accessToken: accessToken, ); + + // Close any previous realtime client that may still be connected due to + // flutter web hot-restart. + if (kDebugMode) { + disposePreviousClient(); + markClientToDispose(client); + } _widgetsBindingInstance?.addObserver(this); _initialized = true; }