Skip to content

Commit 5906239

Browse files
authored
Feature/bump (#9)
* Squashed commit of the following: commit 42abfb5 Author: Plague Fox <[email protected]> Date: Sat Feb 24 02:16:35 2024 +0400 Update lints * Add first message after connection/reconnection * Add interceptors * Fix after connect * Test interceptors * 1.0.0 * Reformat * Remove nodoc * Add missing comments * Add information about options to readme
1 parent d2b8889 commit 5906239

25 files changed

+479
-164
lines changed

.vscode/settings.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"editor.suggest.snippetsPreventQuickSuggestions": false,
55
"editor.suggestSelection": "first",
66
"editor.tabCompletion": "onlySnippets",
7-
"editor.wordBasedSuggestions": false,
7+
"editor.wordBasedSuggestions": "off",
88
"editor.rulers": [80],
99
"editor.defaultFormatter": "Dart-Code.dart-code",
1010
"editor.formatOnPaste": true,

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## 1.0.0
2+
3+
- Stable release
4+
- Add interceptors for incoming and outgoing messages
5+
- Add possibility to send messages right after connection
6+
17
## 1.0.0-pre.6
28

39
- **BREAKING CHANGE**: Change options to separate, platform-specific object

Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ server-down:
2525
# dart run coverage:test_with_coverage -fb -o coverage -- --concurrency=6 --platform chrome,vm --coverage=./coverage --reporter=expanded test/ws_test.dart
2626
coverage: get
2727
@dart test --concurrency=6 --platform chrome,vm --coverage=coverage test/
28-
@dart run coverage:format_coverage --lcov --in=coverage --out=coverage/lcov.info --packages=.packages --report-on=lib
28+
@dart run coverage:format_coverage --lcov --in=coverage --out=coverage/lcov.info --report-on=lib
2929
# @mv coverage/lcov.info coverage/lcov.base.info
3030
# @lcov -r coverage/lcov.base.info -o coverage/lcov.base.info "lib/**.freezed.dart" "lib/**.g.dart"
3131
# @mv coverage/lcov.base.info coverage/lcov.info

README.md

+45-3
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,41 @@ dependencies:
3535
- `add(Object data)`: Sends data on the WebSocket connection. The data can be either a String or a List of integers holding bytes.
3636
- `close([int? code = 1000, String? reason = 'NORMAL_CLOSURE'])`: Permanently stops the WebSocket connection and frees all resources. After calling this method, the WebSocket client is no longer usable.
3737

38+
## Options
39+
40+
The `WebSocketOptions` class provides a configurable set of options for setting up a WebSocket connection on both VM (Virtual Machine) and JS (JavaScript) platforms. This class supports a variety of settings including connection retry strategies, subprotocols, timeouts, message interceptors, and platform-specific configurations.
41+
42+
To use `WebSocketOptions`, instantiate it directly with the desired configuration for your platform. Use the `.vm()` factory for VM platform settings and `.js()` factory for JS platform settings. The `.selector()` factory can be used to construct options depending on the current platform, allowing for flexible configuration across different environments.
43+
44+
### Common Options
45+
46+
Common configuration options applicable across VM and JS platforms:
47+
48+
| Option | Description |
49+
| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
50+
| `connectionRetryInterval` | Specifies the [Backoff full jitter strategy](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/) for reconnecting. Allows tweaks for the reconnect backoff algorithm (min delay, max delay). If not specified, reconnecting is disabled. |
51+
| `protocols` | Specifies the subprotocols the client is willing to speak. |
52+
| `timeout` | Specifies the maximum time to wait for the connection to be established. Defaults to 30 seconds if not specified. |
53+
| `afterConnect` | Specifies the callback function to be called after the connection is established, but before the client is allowed to send user messages. |
54+
| `interceptors` | Specifies the interceptors for WebSocket messages. |
55+
56+
### VM Platform Options
57+
58+
Specific to VM (Mobile, Desktop, Server, Console) platform. Not to be used on the web platform.
59+
60+
- Includes all common options.
61+
- `headers`: Specifies additional HTTP headers for setting up the connection. Certain headers are controlled by the WebSocket connection process and will be ignored if passed.
62+
- `compression`: If provided, configures the WebSocket to negotiate with the specified `CompressionOptions`.
63+
- `customClient`: Allows specifying a custom `HttpClient`.
64+
- `userAgent`: Allows specifying a custom `UserAgent`.
65+
66+
### JS Platform Options
67+
68+
Specific to JS (Browser) platform. Not to be used on the VM platform.
69+
70+
- Includes all common options.
71+
- `useBlobForBinary`: Specifies if `Uint8List` should be sent as `Blob` or as typed data. Defaults to sending as typed data.
72+
3873
## Example
3974

4075
![](example.png)
@@ -69,11 +104,14 @@ The `ws` package provides a cross-platform WebSocket client that supports JSON.
69104
- ✅ Fake client for testing
70105
- ✅ Custom exceptions
71106
- ✅ 90% test coverage
72-
- ❌ First message after connection/reconnection
73-
- ❌ Automatic ping/pong for keep-alive & connection health check
107+
- ✅ First message after connection/reconnection
108+
- ✅ Interceptors (middlewares)
109+
110+
<!--
74111
- ❌ Reusing client between isolates
75-
- Interceptors (middlewares)
112+
- Automatic ping/pong for keep-alive & connection health check
76113
- ❌ RPC support
114+
-->
77115

78116
## More resources
79117

@@ -111,3 +149,7 @@ We appreciate any form of support, whether it's a financial donation or just a s
111149
## Tags
112150

113151
web, socket, ws, wss, WebSocket, cross, platform
152+
153+
```
154+
155+
```

analysis_options.yaml

+1-2
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,9 @@ linter:
7777
close_sinks: true
7878
control_flow_in_finally: true
7979
empty_statements: true
80-
iterable_contains_unrelated_type: true
80+
collection_methods_unrelated_type: true
8181
join_return_with_assignment: true
8282
leading_newlines_in_multiline_strings: true
83-
list_remove_unrelated_type: true
8483
literal_only_boolean_expressions: true
8584
missing_whitespace_between_adjacent_strings: true
8685
no_adjacent_strings_in_list: true

lib/src/client/message_stream.dart

-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
import 'dart:async';
22
import 'dart:convert';
33

4-
/// {@nodoc}
54
final Converter<String, Map<String, Object?>> _$jsonTextDecoder =
65
const JsonDecoder().cast<String, Map<String, Object?>>();
76

8-
/// {@nodoc}
97
final Converter<List<int>, Map<String, Object?>> _$jsonBytesDecoder =
108
const Utf8Decoder().fuse<Map<String, Object?>>(
119
const JsonDecoder().cast<String, Map<String, Object?>>());

lib/src/client/ws_client.dart

+6-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ final class WebSocketClient implements IWebSocketClient {
5353
WebSocketConnectionManager(this);
5454

5555
/// Current options.
56-
/// {@nodoc}
5756
final WebSocketOptions _options;
5857

5958
@override
@@ -124,6 +123,12 @@ final class WebSocketClient implements IWebSocketClient {
124123
stackTrace,
125124
);
126125
}
126+
try {
127+
// Send first messages after connection is established:
128+
await _options.afterConnect?.call(_client);
129+
} on Object {
130+
_client.disconnect(1006, 'AFTER_CONNECT_ERROR');
131+
}
127132
});
128133
}
129134

lib/src/client/ws_client_base.dart

+70-48
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// ignore_for_file: avoid_types_on_closure_parameters, omit_local_variable_types
2+
13
import 'dart:async';
24
import 'dart:collection';
35

@@ -7,31 +9,41 @@ import 'package:ws/src/client/state.dart';
79
import 'package:ws/src/client/state_stream.dart';
810
import 'package:ws/src/client/web_socket_ready_state.dart';
911
import 'package:ws/src/client/ws_client_interface.dart';
12+
import 'package:ws/src/client/ws_interceptor.dart';
1013
import 'package:ws/src/util/constants.dart';
1114
import 'package:ws/src/util/logger.dart';
1215

13-
/// {@nodoc}
1416
@internal
1517
abstract base class WebSocketClientBase implements IWebSocketClient {
16-
/// {@nodoc}
17-
WebSocketClientBase({Iterable<String>? protocols})
18-
: _dataController = StreamController<Object>.broadcast(),
18+
WebSocketClientBase({
19+
Iterable<String>? protocols,
20+
Iterable<WSInterceptor>? interceptors,
21+
}) : _dataController = StreamController<Object>.broadcast(),
1922
_stateController = StreamController<WebSocketClientState>.broadcast(),
2023
_state = WebSocketClientState.initial(),
2124
protocols = protocols != null
22-
? UnmodifiableListView(protocols.toList(growable: false))
23-
: null;
25+
? UnmodifiableListView<String>(protocols.toList(growable: false))
26+
: null {
27+
final chain =
28+
interceptors?.toList(growable: false) ?? const <WSInterceptor>[];
29+
_buildSendChain(chain);
30+
_buildReceiveChain(chain);
31+
}
2432

2533
@override
2634
bool get isClosed => _isClosed; // coverage:ignore-line
2735
bool _isClosed = false;
2836

29-
/// {@nodoc}
3037
@protected
3138
final List<String>? protocols;
3239

40+
/// On message sent callback interceptors chain.
41+
late final void Function(Object data) _onSentChain;
42+
43+
/// On message received callback interceptors chain.
44+
late final void Function(Object data) _onReceivedDataChain;
45+
3346
/// Output stream of data from native WebSocket client.
34-
/// {@nodoc}
3547
@protected
3648
final StreamController<Object> _dataController;
3749

@@ -40,15 +52,13 @@ abstract base class WebSocketClientBase implements IWebSocketClient {
4052
WebSocketMessagesStream(_dataController.stream);
4153

4254
/// Current ready state of the WebSocket connection.
43-
/// {@nodoc}
4455
abstract final WebSocketReadyState readyState;
4556

4657
@override
4758
WebSocketClientState get state => _state;
4859
WebSocketClientState _state;
4960

5061
/// Output stream of state changes.
51-
/// {@nodoc}
5262
@protected
5363
final StreamController<WebSocketClientState> _stateController;
5464

@@ -62,16 +72,14 @@ abstract base class WebSocketClientBase implements IWebSocketClient {
6272
setState((_) => WebSocketClientState.connecting(url: url));
6373
}
6474

75+
@protected
76+
@visibleForOverriding
77+
void push(Object data);
78+
6579
@override
66-
@mustCallSuper
67-
FutureOr<void> add(Object data) async {
68-
// coverage:ignore-start
69-
if ($debugWS) {
70-
var text = data.toString();
71-
text = text.length > 100 ? '${text.substring(0, 97)}...' : text;
72-
fine('> $text');
73-
}
74-
// coverage:ignore-end
80+
@nonVirtual
81+
FutureOr<void> add(Object data) {
82+
_onSentChain(data);
7583
}
7684

7785
@override
@@ -93,12 +101,10 @@ abstract base class WebSocketClientBase implements IWebSocketClient {
93101
try {
94102
await disconnect(code, reason);
95103
} on Object {/* ignore */} // coverage:ignore-line
96-
97104
_dataController.close().ignore();
98105
_stateController.close().ignore();
99106
}
100107

101-
/// {@nodoc}
102108
@protected
103109
void setState(
104110
WebSocketClientState Function(WebSocketClientState state) change) {
@@ -108,42 +114,18 @@ abstract base class WebSocketClientBase implements IWebSocketClient {
108114
info('WebSocketClient state changed to $newState');
109115
}
110116

111-
/// {@nodoc}
112117
@protected
113118
void onConnected(String url) {
114119
setState((_) => WebSocketClientState.open(url: url));
115120
}
116121

117-
/// {@nodoc}
118-
@protected
119-
void onSent(Object data) {
120-
// coverage:ignore-start
121-
if ($debugWS) {
122-
var text = data.toString();
123-
text = text.length > 100 ? '${text.substring(0, 97)}...' : text;
124-
fine('Sent: $text');
125-
}
126-
// coverage:ignore-end
127-
}
128-
129122
/// On data received callback.
130-
/// {@nodoc}
131123
@protected
132124
void onReceivedData(Object? data) {
133-
// coverage:ignore-start
134-
if (data == null || _dataController.isClosed) return;
135-
// coverage:ignore-end
136-
_dataController.add(data);
137-
// coverage:ignore-start
138-
if ($debugWS) {
139-
var text = data.toString();
140-
text = text.length > 100 ? '${text.substring(0, 97)}...' : text;
141-
fine('< $text');
142-
}
143-
// coverage:ignore-end
125+
if (data == null) return;
126+
_onReceivedDataChain(data);
144127
}
145128

146-
/// {@nodoc}
147129
@protected
148130
void onDisconnected(int? code, String? reason) {
149131
setState((_) => WebSocketClientState.closed(
@@ -153,7 +135,47 @@ abstract base class WebSocketClientBase implements IWebSocketClient {
153135
}
154136

155137
/// Error callback.
156-
/// {@nodoc}
157138
@protected
158139
void onError(Object error, StackTrace stackTrace) {}
140+
141+
/// Build push interceptors
142+
void _buildSendChain(List<WSInterceptor> interceptors) {
143+
void Function(Object) fn = (Object data) {
144+
push(data);
145+
// coverage:ignore-start
146+
if ($debugWS) {
147+
var text = data.toString();
148+
text = text.length > 100 ? '${text.substring(0, 97)}...' : text;
149+
fine('> $text');
150+
}
151+
// coverage:ignore-end
152+
};
153+
for (var i = interceptors.length - 1; i >= 0; i--) {
154+
final interceptor = interceptors[i];
155+
final next = fn;
156+
fn = (data) => interceptor.onSend(data, next);
157+
}
158+
_onSentChain = fn;
159+
}
160+
161+
/// Build receive interceptors
162+
void _buildReceiveChain(List<WSInterceptor> interceptors) {
163+
void Function(Object) fn = (Object data) {
164+
if (_dataController.isClosed) return;
165+
_dataController.add(data);
166+
// coverage:ignore-start
167+
if ($debugWS) {
168+
var text = data.toString();
169+
text = text.length > 100 ? '${text.substring(0, 97)}...' : text;
170+
fine('< $text');
171+
}
172+
// coverage:ignore-end
173+
};
174+
for (var i = 0; i < interceptors.length; i++) {
175+
final interceptor = interceptors[i];
176+
final next = fn;
177+
fn = (data) => interceptor.onMessage(data, next);
178+
}
179+
_onReceivedDataChain = fn;
180+
}
159181
}

lib/src/client/ws_client_fake.dart

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import 'package:ws/src/client/state_stream.dart';
77
import 'package:ws/src/client/ws_client_interface.dart';
88
import 'package:ws/src/client/ws_options.dart';
99

10+
/// Platform related callback to create a WebSocket client.
1011
// coverage:ignore-start
11-
/// {@nodoc}
1212
@internal
1313
IWebSocketClient $platformWebSocketClient(WebSocketOptions? options) =>
1414
WebSocketClientFake(protocols: options?.protocols);
@@ -29,7 +29,7 @@ final class WebSocketClientFake implements IWebSocketClient {
2929
_isClosed = false,
3030
_state = WebSocketClientState.initial();
3131

32-
/// {@nodoc}
32+
/// List of protocols to be used in the WebSocket connection.
3333
@visibleForTesting
3434
final List<String>? protocols;
3535

0 commit comments

Comments
 (0)