Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Dispose supabase client after flutter web hot-restart #1142

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
17 changes: 8 additions & 9 deletions packages/gotrue/lib/src/broadcast_web.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,14 @@ BroadcastChannel getBroadcastChannel(String broadcastKey) {
final broadcast = web.BroadcastChannel(broadcastKey);
final controller = StreamController<Map<String, dynamic>>();

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,
Expand Down
3 changes: 2 additions & 1 deletion packages/gotrue/lib/src/gotrue_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down
7 changes: 3 additions & 4 deletions packages/realtime_client/lib/src/realtime_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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'];
Expand Down Expand Up @@ -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),
Expand Down
3 changes: 0 additions & 3 deletions packages/realtime_client/test/socket_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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'),
Expand All @@ -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(
Expand Down
1 change: 1 addition & 0 deletions packages/supabase/lib/src/supabase_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ class SupabaseClient {

Future<void> dispose() async {
_log.fine('Dispose SupabaseClient');
await realtime.disconnect();
await _authStateSubscription?.cancel();
await _isolate.dispose();
_authInstance?.dispose();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import 'package:supabase_flutter/supabase_flutter.dart';

void markClientToDispose(SupabaseClient client) {}

void disposePreviousClient() {}
36 changes: 36 additions & 0 deletions packages/supabase_flutter/lib/src/hot_restart_cleanup_web.dart
Original file line number Diff line number Diff line change
@@ -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;
}
}
10 changes: 10 additions & 0 deletions packages/supabase_flutter/lib/src/supabase.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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;
}
Expand Down