Skip to content

Commit ce582e3

Browse files
authored
fix: Dispose supabase client after flutter web hot-restart (#1142)
* fix: disconnect open realtime client after flutter web hot-restart * refactor: remove unused longpollerTimeout * refactor: remove usage of longpollerTimeout * refactor: only apply manual disconnect in debug mode * refactor: dispose whole supabase client on hot-restart * fix: properly migrate broadcast channel to web package * refactor: rename methods * refactor: rename external js field
1 parent 42fdc91 commit ce582e3

File tree

8 files changed

+65
-17
lines changed

8 files changed

+65
-17
lines changed

packages/gotrue/lib/src/broadcast_web.dart

+8-9
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,14 @@ BroadcastChannel getBroadcastChannel(String broadcastKey) {
1212
final broadcast = web.BroadcastChannel(broadcastKey);
1313
final controller = StreamController<Map<String, dynamic>>();
1414

15-
broadcast.addEventListener(
16-
'message',
17-
(web.Event event) {
18-
if (event is web.MessageEvent) {
19-
final dataMap = event.data.dartify();
20-
controller.add(json.decode(json.encode(dataMap)));
21-
}
22-
} as web.EventListener,
23-
);
15+
void onMessage(web.Event event) {
16+
if (event is web.MessageEvent) {
17+
final dataMap = event.data.dartify();
18+
controller.add(json.decode(json.encode(dataMap)));
19+
}
20+
}
21+
22+
broadcast.onmessage = onMessage.toJS;
2423

2524
return (
2625
onMessage: controller.stream,

packages/gotrue/lib/src/gotrue_client.dart

+2-1
Original file line numberDiff line numberDiff line change
@@ -1205,7 +1205,8 @@ class GoTrueClient {
12051205
notifyAllSubscribers(event, session: session, broadcast: false);
12061206
}
12071207
});
1208-
} catch (e) {
1208+
} catch (error, stackTrace) {
1209+
_log.warning('Failed to start broadcast channel', error, stackTrace);
12091210
// Ignoring
12101211
}
12111212
}

packages/realtime_client/lib/src/realtime_client.dart

+3-4
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ class RealtimeClient {
8888
'error': [],
8989
'message': []
9090
};
91+
92+
@Deprecated("No longer used. Will be removed in the next major version.")
9193
int longpollerTimeout = 20000;
9294
SocketStates? connState;
9395
// This is called `accessToken` in realtime-js
@@ -113,8 +115,6 @@ class RealtimeClient {
113115
///
114116
/// [decode] The function to decode incoming messages. Defaults to JSON: (payload, callback) => callback(JSON.parse(payload))
115117
///
116-
/// [longpollerTimeout] The maximum timeout of a long poll AJAX request. Defaults to 20s (double the server long poll timer).
117-
///
118118
/// [reconnectAfterMs] The optional function that returns the millsec reconnect interval. Defaults to stepped backoff off.
119119
///
120120
/// [logLevel] Specifies the log level for the connection on the server.
@@ -145,7 +145,7 @@ class RealtimeClient {
145145
},
146146
transport = transport ?? createWebSocketClient {
147147
_log.config(
148-
'Initialize RealtimeClient with endpoint: $endPoint, timeout: $timeout, heartbeatIntervalMs: $heartbeatIntervalMs, longpollerTimeout: $longpollerTimeout, logLevel: $logLevel');
148+
'Initialize RealtimeClient with endpoint: $endPoint, timeout: $timeout, heartbeatIntervalMs: $heartbeatIntervalMs, logLevel: $logLevel');
149149
_log.finest('Initialize with headers: $headers, params: $params');
150150
final customJWT = this.headers['Authorization']?.split(' ').last;
151151
accessToken = customJWT ?? params['apikey'];
@@ -198,7 +198,6 @@ class RealtimeClient {
198198
connState = SocketStates.open;
199199

200200
_onConnOpen();
201-
conn!.stream.timeout(Duration(milliseconds: longpollerTimeout));
202201
conn!.stream.listen(
203202
// incoming messages
204203
(message) => onConnMessage(message as String),

packages/realtime_client/test/socket_test.dart

-3
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,6 @@ void main() {
7878
'message': [],
7979
});
8080
expect(socket.timeout, const Duration(milliseconds: 10000));
81-
expect(socket.longpollerTimeout, 20000);
8281
expect(socket.heartbeatIntervalMs, Constants.defaultHeartbeatIntervalMs);
8382
expect(
8483
socket.logger is void Function(
@@ -99,7 +98,6 @@ void main() {
9998
final socket = RealtimeClient(
10099
'wss://example.com/socket',
101100
timeout: const Duration(milliseconds: 40000),
102-
longpollerTimeout: 50000,
103101
heartbeatIntervalMs: 60000,
104102
// ignore: avoid_print
105103
logger: (kind, msg, data) => print('[$kind] $msg $data'),
@@ -116,7 +114,6 @@ void main() {
116114
'message': [],
117115
});
118116
expect(socket.timeout, const Duration(milliseconds: 40000));
119-
expect(socket.longpollerTimeout, 50000);
120117
expect(socket.heartbeatIntervalMs, 60000);
121118
expect(
122119
socket.logger is void Function(

packages/supabase/lib/src/supabase_client.dart

+1
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ class SupabaseClient {
268268

269269
Future<void> dispose() async {
270270
_log.fine('Dispose SupabaseClient');
271+
await realtime.disconnect();
271272
await _authStateSubscription?.cancel();
272273
await _isolate.dispose();
273274
_authInstance?.dispose();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import 'package:supabase_flutter/supabase_flutter.dart';
2+
3+
void markClientToDispose(SupabaseClient client) {}
4+
5+
void disposePreviousClient() {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import 'dart:js_interop';
2+
3+
import 'package:supabase_flutter/supabase_flutter.dart';
4+
5+
@JS()
6+
external JSFunction? supabaseFlutterClientToDispose;
7+
8+
/// Store a function to properly dispose the previous [SupabaseClient] in
9+
/// the js context.
10+
///
11+
/// WebSocket connections and [BroadcastChannel] are not closed when Flutter is hot-restarted on web.
12+
///
13+
/// This causes old dart code that is still associated with those
14+
/// connections to be still running and causes unexpected behavior like type
15+
/// errors and the fact that the events of the old connection may still be
16+
/// logged.
17+
void markClientToDispose(SupabaseClient client) {
18+
void dispose() {
19+
client.realtime.disconnect(
20+
code: 1000, reason: 'Closed due to Flutter Web hot-restart');
21+
client.dispose();
22+
}
23+
24+
supabaseFlutterClientToDispose = dispose.toJS;
25+
}
26+
27+
/// Disconnect the previous [SupabaseClient] if it exists.
28+
///
29+
/// This is done by calling the function stored by
30+
/// [markClientToDispose] from the js context
31+
void disposePreviousClient() {
32+
if (supabaseFlutterClientToDispose != null) {
33+
supabaseFlutterClientToDispose!.callAsFunction();
34+
supabaseFlutterClientToDispose = null;
35+
}
36+
}

packages/supabase_flutter/lib/src/supabase.dart

+10
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ import 'package:supabase_flutter/src/flutter_go_true_client_options.dart';
1111
import 'package:supabase_flutter/src/local_storage.dart';
1212
import 'package:supabase_flutter/src/supabase_auth.dart';
1313

14+
import 'hot_restart_cleanup_stub.dart'
15+
if (dart.library.js_interop) 'hot_restart_cleanup_web.dart';
16+
1417
import 'version.dart';
1518

1619
final _log = Logger('supabase.supabase_flutter');
@@ -203,6 +206,13 @@ class Supabase with WidgetsBindingObserver {
203206
authOptions: authOptions,
204207
accessToken: accessToken,
205208
);
209+
210+
// Close any previous realtime client that may still be connected due to
211+
// flutter web hot-restart.
212+
if (kDebugMode) {
213+
disposePreviousClient();
214+
markClientToDispose(client);
215+
}
206216
_widgetsBindingInstance?.addObserver(this);
207217
_initialized = true;
208218
}

0 commit comments

Comments
 (0)