Skip to content

Commit af33380

Browse files
authored
Options (#4)
- **BREAKING CHANGE**: Change options to separate, platform-specific object - You can now pass a headers and other options to the IO websocket client - Backoff full jitter strategy for reconnection attempts - Add more metrics - Public Fake WebSocket client for testing
1 parent fdbd876 commit af33380

25 files changed

+641
-165
lines changed

CHANGELOG.md

+8
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
## 1.0.0-pre.1
2+
3+
- **BREAKING CHANGE**: Change options to separate, platform-specific object
4+
- You can now pass a headers and other options to the IO websocket client
5+
- Backoff full jitter strategy for reconnection attempts
6+
- Add more metrics
7+
- Public Fake WebSocket client for testing
8+
19
## 0.1.2
210

311
- Update README.md

example/ws_example.dart

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ void main([List<String>? args]) {
1010
defaultValue: 'wss://echo.plugfox.dev:443/connect');
1111

1212
// Setup a WebSocket client with auto reconnect
13-
final client = WebSocketClient(reconnectTimeout: const Duration(seconds: 5))
13+
final client = WebSocketClient()
1414
// Observing the incoming messages from the server
1515
..stream.listen((message) => print('< $message'))
1616
// Observing the state changes (connecting, open, disconnecting, closed)

lib/interface.dart

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
library dev.plugfox.ws.interface;
1+
library interface;
22

33
export 'package:ws/src/client/message_stream.dart';
44
export 'package:ws/src/client/metrics.dart';
55
export 'package:ws/src/client/state.dart';
66
export 'package:ws/src/client/status_codes.dart';
77
export 'package:ws/src/client/web_socket_ready_state.dart';
8+
export 'package:ws/src/client/ws_client_fake.dart' show WebSocketClientFake;
89
export 'package:ws/src/client/ws_client_interface.dart';
10+
export 'package:ws/src/client/ws_options.dart';

lib/src/client/message_stream.dart

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ final Converter<List<int>, Map<String, Object?>> _$jsonBytesDecoder =
1111
const JsonDecoder().cast<String, Map<String, Object?>>());
1212

1313
/// Stream of message events handled by this WebSocket.
14+
/// {@category Client}
15+
/// {@category Entity}
1416
final class WebSocketMessagesStream
1517
extends StreamView< /* String || List<int> */ Object> {
1618
/// Stream of message events handled by this WebSocket.

lib/src/client/metrics.dart

+33-20
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ final class WebSocketMetrics {
1010
const WebSocketMetrics({
1111
required this.timestamp,
1212
required this.readyState,
13-
required this.reconnectTimeout,
13+
required this.nextReconnectionAttempt,
1414
required this.transferredSize,
1515
required this.receivedSize,
1616
required this.transferredCount,
@@ -19,8 +19,9 @@ final class WebSocketMetrics {
1919
required this.lastSuccessfulConnectionTime,
2020
required this.disconnects,
2121
required this.lastDisconnectTime,
22-
required this.expectedReconnectTime,
2322
required this.lastDisconnect,
23+
required this.isReconnectionActive,
24+
required this.currentReconnectAttempts,
2425
required this.lastUrl,
2526
});
2627

@@ -51,11 +52,6 @@ final class WebSocketMetrics {
5152
WebSocketReadyState.fromCode,
5253
() => WebSocketReadyState.closed,
5354
),
54-
reconnectTimeout: extract<int, Duration>(
55-
'reconnectTimeout',
56-
(v) => Duration(milliseconds: v),
57-
() => Duration.zero,
58-
),
5955
transferredSize: extract<String, BigInt>(
6056
'transferredSize',
6157
BigInt.parse,
@@ -120,8 +116,18 @@ final class WebSocketMetrics {
120116
(v) => v,
121117
() => null,
122118
),
123-
expectedReconnectTime: extract<int, DateTime?>(
124-
'expectedReconnectTime',
119+
isReconnectionActive: extract<bool, bool>(
120+
'isReconnectionActive',
121+
(v) => v,
122+
() => false,
123+
),
124+
currentReconnectAttempts: extract<int, int>(
125+
'currentReconnectAttempts',
126+
(v) => v,
127+
() => 0,
128+
),
129+
nextReconnectionAttempt: extract<int, DateTime?>(
130+
'nextReconnectionAttempt',
125131
DateTime.fromMillisecondsSinceEpoch,
126132
() => null,
127133
),
@@ -132,7 +138,6 @@ final class WebSocketMetrics {
132138
Map<String, Object?> toJson() => <String, Object?>{
133139
'timestamp': timestamp.millisecondsSinceEpoch,
134140
'readyState': readyState.code,
135-
'reconnectTimeout': reconnectTimeout.inMilliseconds,
136141
'transferredSize': transferredSize.toString(),
137142
'receivedSize': receivedSize.toString(),
138143
'transferredCount': transferredCount.toString(),
@@ -143,9 +148,12 @@ final class WebSocketMetrics {
143148
lastSuccessfulConnectionTime?.millisecondsSinceEpoch,
144149
'disconnects': disconnects,
145150
'lastDisconnectTime': lastDisconnectTime?.millisecondsSinceEpoch,
146-
'expectedReconnectTime': expectedReconnectTime?.millisecondsSinceEpoch,
151+
'nextReconnectionAttempt':
152+
nextReconnectionAttempt?.millisecondsSinceEpoch,
147153
'lastDisconnectCode': lastDisconnect.code,
148154
'lastDisconnectReason': lastDisconnect.reason,
155+
'isReconnectionActive': isReconnectionActive,
156+
'currentReconnectAttempts': currentReconnectAttempts,
149157
'lastUrl': lastUrl,
150158
};
151159

@@ -155,9 +163,6 @@ final class WebSocketMetrics {
155163
/// The current state of the connection.
156164
final WebSocketReadyState readyState;
157165

158-
/// Timeout between reconnection attempts.
159-
final Duration reconnectTimeout;
160-
161166
/// The total number of bytes sent.
162167
final BigInt transferredSize;
163168

@@ -183,18 +188,23 @@ final class WebSocketMetrics {
183188
final DateTime? lastDisconnectTime;
184189

185190
/// The time of the next expected reconnect.
186-
final DateTime? expectedReconnectTime;
191+
final DateTime? nextReconnectionAttempt;
187192

188193
/// The last disconnect reason.
189194
final ({int? code, String? reason}) lastDisconnect;
190195

196+
/// Is the client currently planning to reconnect?
197+
final bool isReconnectionActive;
198+
199+
/// The current number of reconnection attempts.
200+
final int currentReconnectAttempts;
201+
191202
/// The last URL used to connect.
192203
final String? lastUrl;
193204

194205
@override
195206
int get hashCode => Object.hashAll([
196207
readyState,
197-
reconnectTimeout,
198208
transferredSize,
199209
receivedSize,
200210
transferredCount,
@@ -203,8 +213,10 @@ final class WebSocketMetrics {
203213
lastSuccessfulConnectionTime,
204214
disconnects,
205215
lastDisconnectTime,
206-
expectedReconnectTime,
216+
nextReconnectionAttempt,
207217
lastDisconnect,
218+
isReconnectionActive,
219+
currentReconnectAttempts,
208220
lastUrl,
209221
]);
210222

@@ -219,19 +231,20 @@ final class WebSocketMetrics {
219231
'${ago ? 'ago' : 'from now'}'
220232
: 'never';
221233
return '- readyState: ${readyState.name}\n'
222-
'- reconnectTimeout: ${reconnectTimeout.inSeconds} seconds\n'
223234
'- transferredSize: $transferredSize\n'
224235
'- receivedSize: $receivedSize\n'
225236
'- transferredCount: $transferredCount\n'
226237
'- receivedCount: $receivedCount\n'
238+
'- isReconnectionActive: $isReconnectionActive\n'
239+
'- currentReconnectAttempts: $currentReconnectAttempts\n'
227240
'- reconnects: ${reconnects.successful} / ${reconnects.total}\n'
228241
'- lastSuccessfulConnectionTime: '
229242
'${dateTimeRepresentation(lastSuccessfulConnectionTime, ago: true)}\n'
230243
'- disconnects: $disconnects\n'
231244
'- lastDisconnectTime: '
232245
'${dateTimeRepresentation(lastDisconnectTime, ago: true)}\n'
233-
'- expectedReconnectTime: '
234-
'${dateTimeRepresentation(expectedReconnectTime)}\n'
246+
'- nextReconnectionAttempt: '
247+
'${dateTimeRepresentation(nextReconnectionAttempt)}\n'
235248
'- lastDisconnect: '
236249
'${lastDisconnect.code ?? 'unknown'} '
237250
'(${lastDisconnect.reason ?? 'unknown'})\n'

lib/src/client/state.dart

+1-14
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,11 @@ import 'package:meta/meta.dart';
22
import 'package:ws/src/client/status_codes.dart';
33
import 'package:ws/src/client/web_socket_ready_state.dart';
44

5-
/// Whether the stream controller is permanently closed.
6-
///
7-
/// The controller becomes closed by calling the [close] method.
8-
///
9-
/// If the controller is closed,
10-
/// the "done" event might not have been delivered yet,
11-
/// but it has been scheduled, and it is too late to add more events.
12-
//bool get isClosed;
13-
14-
/// A future which is completed when the stream controller is done.
15-
//Future<void> get done;
16-
175
/// {@template web_socket_client_state}
186
/// WebSocket client state.
19-
///
7+
/// {@endtemplate}
208
/// {@category Client}
219
/// {@category Entity}
22-
/// {@endtemplate}
2310
@immutable
2411
sealed class WebSocketClientState {
2512
/// {@macro web_socket_client_state}

lib/src/client/status_codes.dart

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import 'package:meta/meta.dart';
2424
/// and thus can't be registered. Such codes can be used by prior
2525
/// agreements between WebSocket applications. The interpretation of
2626
/// these codes is undefined by this protocol.
27+
/// {@category Client}
2728
/// {@category Entity}
2829
enum WebSocketStatusCodes implements Comparable<WebSocketStatusCodes> {
2930
/// Successful operation / regular socket shutdown.

lib/src/client/web_socket_ready_state.dart

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/// The [IWebSocketPlatformTransport.readyState] property
22
/// returns the current state of the WebSocket connection.
3+
/// {@category Client}
34
/// {@category Entity}
45
enum WebSocketReadyState {
56
/// Socket has been created. The connection is not yet open.

lib/src/client/websocket_exception.dart

+7
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
/// This is a custom exception class for WebSocket.
33
/// Do not confuse it with a native exception WebSocketException from dart:io
44
/// {@endtemplate}
5+
/// {@category Client}
56
/// {@category Entity}
67
sealed class WSException implements Exception {
78
/// {@macro websocket_exception}
@@ -17,6 +18,7 @@ sealed class WSException implements Exception {
1718
/// {@template not_connected_exception}
1819
/// Exception thrown when a WebSocket is not connected.
1920
/// {@endtemplate}
21+
/// {@category Client}
2022
/// {@category Entity}
2123
final class WSNotConnected extends WSException {
2224
/// {@macro not_connected_exception}
@@ -26,6 +28,7 @@ final class WSNotConnected extends WSException {
2628
/// {@template unknown_exception}
2729
/// Unknown WebSocket exception.
2830
/// {@endtemplate}
31+
/// {@category Client}
2932
/// {@category Entity}
3033
final class WSUnknownException extends WSException {
3134
/// {@macro unknown_exception}
@@ -36,6 +39,7 @@ final class WSUnknownException extends WSException {
3639
/// {@template socket_exception}
3740
/// Exception thrown when a socket operation fails.
3841
/// {@endtemplate}
42+
/// {@category Client}
3943
/// {@category Entity}
4044
final class WSSocketException extends WSException {
4145
/// {@macro socket_exception}
@@ -45,6 +49,7 @@ final class WSSocketException extends WSException {
4549
/// {@template http_exception}
4650
/// Exception thrown when a socket operation fails.
4751
/// {@endtemplate}
52+
/// {@category Client}
4853
/// {@category Entity}
4954
final class WSHttpException extends WSException {
5055
/// {@macro http_exception}
@@ -54,6 +59,7 @@ final class WSHttpException extends WSException {
5459
/// {@template unsupported_exception}
5560
/// The operation was not allowed by the object.
5661
/// {@endtemplate}
62+
/// {@category Client}
5763
/// {@category Entity}
5864
final class WSUnsupportedException extends WSException {
5965
/// {@macro unsupported_exception}
@@ -63,6 +69,7 @@ final class WSUnsupportedException extends WSException {
6369
/// {@template client_closed}
6470
/// The operation was not allowed by the object.
6571
/// {@endtemplate}
72+
/// {@category Client}
6673
/// {@category Entity}
6774
final class WSClientClosed extends WSException implements StateError {
6875
/// {@macro client_closed}

lib/src/client/ws_client.dart

+24-20
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
import 'dart:async';
22

3+
import 'package:ws/src/client/message_stream.dart';
4+
import 'package:ws/src/client/metrics.dart';
5+
import 'package:ws/src/client/state.dart';
6+
import 'package:ws/src/client/websocket_exception.dart';
37
import 'package:ws/src/client/ws_client_fake.dart'
48
// ignore: uri_does_not_exist
59
if (dart.library.html) 'package:ws/src/client/ws_client_js.dart'
610
// ignore: uri_does_not_exist
7-
if (dart.library.io) 'package:ws/src/client/ws_client_io.dart';
11+
if (dart.library.io) 'package:ws/src/client/ws_client_vm.dart';
812
import 'package:ws/src/client/ws_client_interface.dart';
13+
import 'package:ws/src/client/ws_options.dart';
914
import 'package:ws/src/manager/connection_manager.dart';
1015
import 'package:ws/src/manager/metrics_manager.dart';
1116
import 'package:ws/src/util/event_queue.dart';
12-
import 'package:ws/ws.dart';
1317

1418
/// {@template ws_client}
1519
/// WebSocket client.
@@ -19,11 +23,9 @@ import 'package:ws/ws.dart';
1923
/// {@category Client}
2024
final class WebSocketClient implements IWebSocketClient {
2125
/// {@macro ws_client}
22-
WebSocketClient(
23-
{Duration reconnectTimeout = const Duration(seconds: 5),
24-
Iterable<String>? protocols})
25-
: reconnectTimeout = reconnectTimeout.abs(),
26-
_client = $platformWebSocketClient(reconnectTimeout.abs(), protocols) {
26+
WebSocketClient([WebSocketOptions? options])
27+
: _client = $platformWebSocketClient(options),
28+
_options = options {
2729
WebSocketMetricsManager.instance.startObserving(this);
2830
}
2931

@@ -32,32 +34,30 @@ final class WebSocketClient implements IWebSocketClient {
3234
/// with reconnecting and concurrency protection.
3335
/// {@macro ws_client}
3436
WebSocketClient.fromClient(IWebSocketClient client,
35-
{Duration reconnectTimeout = const Duration(seconds: 5)})
36-
: reconnectTimeout = reconnectTimeout.abs(),
37-
_client = client {
37+
[WebSocketOptions? options])
38+
: _client = client,
39+
_options = options {
3840
WebSocketMetricsManager.instance.startObserving(this);
3941
}
4042

4143
/// {@macro ws_client}
42-
factory WebSocketClient.connect(String url,
43-
{Duration reconnectTimeout = const Duration(seconds: 5),
44-
Iterable<String>? protocols}) =>
45-
WebSocketClient(reconnectTimeout: reconnectTimeout, protocols: protocols)
46-
..connect(url).ignore();
44+
factory WebSocketClient.connect(String url, [WebSocketOptions? options]) =>
45+
WebSocketClient(options)..connect(url).ignore();
4746

4847
final IWebSocketClient _client;
4948
final WebSocketEventQueue _eventQueue = WebSocketEventQueue();
5049

50+
/// Current options.
51+
/// {@nodoc}
52+
final WebSocketOptions? _options;
53+
5154
@override
5255
bool get isClosed => _isClosed;
5356
bool _isClosed = false;
5457

55-
@override
56-
final Duration reconnectTimeout;
57-
5858
/// Get the metrics for this client.
5959
WebSocketMetrics get metrics =>
60-
WebSocketMetricsManager.instance.buildMetric(this);
60+
WebSocketMetricsManager.instance.buildMetricFor(this);
6161

6262
@override
6363
WebSocketMessagesStream get stream => _client.stream;
@@ -79,7 +79,11 @@ final class WebSocketClient implements IWebSocketClient {
7979
Future<void> connect(String url) {
8080
if (_isClosed) return Future<void>.error(const WSClientClosed());
8181
return _eventQueue.push('connect', () {
82-
WebSocketConnectionManager.instance.startMonitoringConnection(this, url);
82+
WebSocketConnectionManager.instance.startMonitoringConnection(
83+
this,
84+
url,
85+
_options?.connectionRetryInterval,
86+
);
8387
return _client.connect(url);
8488
});
8589
}

lib/src/client/ws_client_base.dart

+1-6
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,7 @@ import 'package:ws/src/util/logger.dart';
1010
@internal
1111
abstract base class WebSocketClientBase implements IWebSocketClient {
1212
/// {@nodoc}
13-
WebSocketClientBase(
14-
{this.reconnectTimeout = const Duration(seconds: 5),
15-
Iterable<String>? protocols})
13+
WebSocketClientBase({Iterable<String>? protocols})
1614
: _dataController = StreamController<Object>.broadcast(),
1715
_stateController = StreamController<WebSocketClientState>.broadcast(),
1816
_state = WebSocketClientState.initial(),
@@ -28,9 +26,6 @@ abstract base class WebSocketClientBase implements IWebSocketClient {
2826
@protected
2927
final List<String>? protocols;
3028

31-
@override
32-
final Duration reconnectTimeout;
33-
3429
/// Output stream of data from native WebSocket client.
3530
/// {@nodoc}
3631
@protected

0 commit comments

Comments
 (0)