Skip to content

Commit 74fecb2

Browse files
committed
Merge remote-tracking branch 'origin/master' into v10.0.0
# Conflicts: # packages/stream_chat/CHANGELOG.md # packages/stream_chat/test/src/client/channel_test.dart # packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/stream_poll_creator_dialog_dark.png # packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/stream_poll_creator_dialog_light.png # packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/stream_poll_creator_full_screen_dialog_dark.png # packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/stream_poll_creator_full_screen_dialog_light.png # packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_creator_dialog_dark.png # packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_creator_dialog_light.png # packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_creator_full_screen_dialog_dark.png # packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_creator_full_screen_dialog_light.png
2 parents 4ca7fe9 + d666475 commit 74fecb2

28 files changed

+1084
-876
lines changed

packages/stream_chat/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
🐞 Fixed
44

55
- Fixed `skipPush` and `skipEnrichUrl` not preserving during message send or update retry
6+
- Fixed `Channel` methods to throw proper `StateError` exceptions instead of relying on assertions
7+
for state validation.
68

79
## 10.0.0-beta.4
810

packages/stream_chat/lib/src/client/channel.dart

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2132,15 +2132,18 @@ class Channel {
21322132
void dispose() {
21332133
client.state.removeChannel('$cid');
21342134
state?.dispose();
2135+
state = null;
21352136
_muteExpirationTimer?.cancel();
21362137
_keyStrokeHandler.cancel();
21372138
}
21382139

21392140
void _checkInitialized() {
2140-
assert(
2141-
_initializedCompleter.isCompleted,
2142-
"Channel $cid hasn't been initialized yet. Make sure to call .watch()"
2143-
' or to instantiate the client using [Channel.fromState]',
2141+
if (_initializedCompleter.isCompleted && state != null) return;
2142+
2143+
throw StateError(
2144+
"Channel $cid hasn't been initialized yet or has been disposed. "
2145+
'Make sure to call .watch() or instantiate the client using '
2146+
'[Channel.fromState]',
21442147
);
21452148
}
21462149
}

packages/stream_chat/lib/src/client/client.dart

Lines changed: 37 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,13 @@ class StreamChatClient {
129129
},
130130
);
131131

132+
_connectionStatusSubscription = wsConnectionStatusStream.pairwise().listen(
133+
(statusPair) {
134+
final [prevStatus, currStatus] = statusPair;
135+
return _onConnectionStatusChanged(prevStatus, currStatus);
136+
},
137+
);
138+
132139
state = ClientState(this);
133140
}
134141

@@ -226,7 +233,7 @@ class StreamChatClient {
226233
///```
227234
final LogHandlerFunction logHandlerFunction;
228235

229-
StreamSubscription<ConnectionStatus>? _connectionStatusSubscription;
236+
StreamSubscription<List<ConnectionStatus>>? _connectionStatusSubscription;
230237

231238
/// Stream of [Event] coming from [_ws] connection
232239
/// Listen to this or use the [on] method to filter specific event types
@@ -242,20 +249,14 @@ class StreamChatClient {
242249
],
243250
);
244251

245-
final _wsConnectionStatusController =
246-
BehaviorSubject.seeded(ConnectionStatus.disconnected);
247-
248-
set _wsConnectionStatus(ConnectionStatus status) =>
249-
_wsConnectionStatusController.add(status);
250-
251252
/// The current status value of the [_ws] connection
252-
ConnectionStatus get wsConnectionStatus =>
253-
_wsConnectionStatusController.value;
253+
ConnectionStatus get wsConnectionStatus => _ws.connectionStatus;
254254

255255
/// This notifies the connection status of the [_ws] connection.
256256
/// Listen to this to get notified when the [_ws] tries to reconnect.
257-
Stream<ConnectionStatus> get wsConnectionStatusStream =>
258-
_wsConnectionStatusController.stream.distinct();
257+
Stream<ConnectionStatus> get wsConnectionStatusStream {
258+
return _ws.connectionStatusStream.distinct();
259+
}
259260

260261
/// Default log handler function for the [StreamChatClient] logger.
261262
static void defaultLogHandler(LogRecord record) {
@@ -451,16 +452,6 @@ class StreamChatClient {
451452
throw StreamChatError('Connection already available for ${user.id}');
452453
}
453454

454-
_wsConnectionStatus = ConnectionStatus.connecting;
455-
456-
// skipping `ws` seed connection status -> ConnectionStatus.disconnected
457-
// otherwise `client.wsConnectionStatusStream` will emit in order
458-
// 1. ConnectionStatus.disconnected -> client seed status
459-
// 2. ConnectionStatus.connecting -> client connecting status
460-
// 3. ConnectionStatus.disconnected -> ws seed status
461-
_connectionStatusSubscription =
462-
_ws.connectionStatusStream.skip(1).listen(_connectionStatusHandler);
463-
464455
try {
465456
final event = await _ws.connect(
466457
user,
@@ -483,13 +474,7 @@ class StreamChatClient {
483474
/// This will not trigger default auto-retry mechanism for reconnection.
484475
/// You need to call [openConnection] to reconnect to [_ws].
485476
void closeConnection() {
486-
if (wsConnectionStatus == ConnectionStatus.disconnected) return;
487-
488477
logger.info('Closing web-socket connection for ${state.currentUser?.id}');
489-
_wsConnectionStatus = ConnectionStatus.disconnected;
490-
491-
_connectionStatusSubscription?.cancel();
492-
_connectionStatusSubscription = null;
493478

494479
// Stop listening to events
495480
state.cancelEventSubscription();
@@ -517,19 +502,25 @@ class StreamChatClient {
517502
return _eventController.add(event);
518503
}
519504

520-
void _connectionStatusHandler(ConnectionStatus status) async {
521-
final previousState = wsConnectionStatus;
522-
final currentState = _wsConnectionStatus = status;
505+
void _onConnectionStatusChanged(
506+
ConnectionStatus prevStatus,
507+
ConnectionStatus currStatus,
508+
) async {
509+
// If the status hasn't changed, we don't need to do anything.
510+
if (prevStatus == currStatus) return;
523511

524-
if (previousState != currentState) {
525-
handleEvent(Event(
526-
type: EventType.connectionChanged,
527-
online: status == ConnectionStatus.connected,
528-
));
529-
}
512+
final wasConnected = prevStatus == ConnectionStatus.connected;
513+
final isConnected = currStatus == ConnectionStatus.connected;
514+
515+
// Notify the connection status change event
516+
handleEvent(Event(
517+
type: EventType.connectionChanged,
518+
online: isConnected,
519+
));
530520

531-
if (currentState == ConnectionStatus.connected &&
532-
previousState != ConnectionStatus.connected) {
521+
final connectionRecovered = !wasConnected && isConnected;
522+
523+
if (connectionRecovered) {
533524
// connection recovered
534525
final cids = state.channels.keys.toList(growable: false);
535526
if (cids.isNotEmpty) {
@@ -2046,6 +2037,9 @@ class StreamChatClient {
20462037
Future<void> disconnectUser({bool flushChatPersistence = false}) async {
20472038
logger.info('Disconnecting user : ${state.currentUser?.id}');
20482039

2040+
// closing web-socket connection
2041+
closeConnection();
2042+
20492043
// resetting state.
20502044
state.dispose();
20512045
state = ClientState(this);
@@ -2056,27 +2050,17 @@ class StreamChatClient {
20562050
_connectionIdManager.reset();
20572051

20582052
// closing persistence connection.
2059-
await closePersistenceConnection(flush: flushChatPersistence);
2060-
2061-
// closing web-socket connection
2062-
return closeConnection();
2053+
return closePersistenceConnection(flush: flushChatPersistence);
20632054
}
20642055

20652056
/// Call this function to dispose the client
20662057
Future<void> dispose() async {
2067-
logger.info('Disposing new StreamChatClient');
2068-
2069-
// disposing state.
2070-
state.dispose();
2071-
2072-
// closing persistence connection.
2073-
await closePersistenceConnection();
2074-
2075-
// closing web-socket connection.
2076-
closeConnection();
2058+
logger.info('Disposing StreamChatClient');
20772059

2060+
await disconnectUser();
2061+
await _ws.dispose();
20782062
await _eventController.close();
2079-
await _wsConnectionStatusController.close();
2063+
await _connectionStatusSubscription?.cancel();
20802064
}
20812065
}
20822066

packages/stream_chat/lib/src/ws/websocket.dart

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class WebSocket with TimerHelper {
4343
this.reconnectionMonitorInterval = 10,
4444
this.healthCheckInterval = 20,
4545
this.reconnectionMonitorTimeout = 40,
46+
this.maxReconnectAttempts = 6,
4647
this.queryParameters = const {},
4748
}) : _logger = logger;
4849

@@ -93,6 +94,11 @@ class WebSocket with TimerHelper {
9394
/// connection unhealthy
9495
final int reconnectionMonitorTimeout;
9596

97+
/// Maximum number of reconnection attempts before giving up.
98+
///
99+
/// Default is 6 attempts. ~30 seconds of reconnection attempts
100+
final int maxReconnectAttempts;
101+
96102
User? _user;
97103
String? _connectionId;
98104
DateTime? _lastEventAt;
@@ -105,18 +111,20 @@ class WebSocket with TimerHelper {
105111
///
106112
String? get connectionId => _connectionId;
107113

108-
final _connectionStatusController =
109-
BehaviorSubject.seeded(ConnectionStatus.disconnected);
110-
111-
set _connectionStatus(ConnectionStatus status) =>
112-
_connectionStatusController.safeAdd(status);
114+
final _connectionStatusController = BehaviorSubject.seeded(
115+
ConnectionStatus.disconnected,
116+
);
113117

114118
/// The current connection status value
115119
ConnectionStatus get connectionStatus => _connectionStatusController.value;
120+
set _connectionStatus(ConnectionStatus status) {
121+
_connectionStatusController.safeAdd(status);
122+
}
116123

117124
/// This notifies of connection status changes
118-
Stream<ConnectionStatus> get connectionStatusStream =>
119-
_connectionStatusController.stream.distinct();
125+
Stream<ConnectionStatus> get connectionStatusStream {
126+
return _connectionStatusController.stream.distinct();
127+
}
120128

121129
void _initWebSocketChannel(Uri uri) {
122130
_logger?.info('Initiating connection with $baseUrl');
@@ -250,6 +258,12 @@ class WebSocket with TimerHelper {
250258
void _reconnect({bool refreshToken = false}) async {
251259
_logger?.info('Retrying connection : $_reconnectAttempt');
252260
if (_reconnectRequestInProgress) return;
261+
262+
if (_reconnectAttempt >= maxReconnectAttempts) {
263+
_logger?.severe('Max reconnect attempts reached: $maxReconnectAttempts');
264+
return disconnect();
265+
}
266+
253267
_reconnectRequestInProgress = true;
254268

255269
_stopMonitoringEvents();
@@ -495,4 +509,14 @@ class WebSocket with TimerHelper {
495509

496510
_closeWebSocketChannel();
497511
}
512+
513+
/// Disposes the web-socket connection and releases resources
514+
Future<void> dispose() async {
515+
_logger?.info('Disposing web-socket connection');
516+
517+
_stopMonitoringEvents();
518+
_unsubscribeFromWebSocketChannel();
519+
_closeWebSocketChannel();
520+
_connectionStatusController.close();
521+
}
498522
}

0 commit comments

Comments
 (0)