Skip to content

Commit 01817d2

Browse files
authored
Create an adapter for package:web_socket (dart-archive/web_socket_channel#339)
1 parent ccfa26f commit 01817d2

11 files changed

+363
-62
lines changed

pkgs/web_socket_channel/CHANGELOG.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
1+
## 3.0.0-wip
2+
3+
- Provide an adapter around `package:web_socket` `WebSocket`s and make it the
4+
default implementation for `WebSocketChannel.connect`.
5+
16
## 2.4.5
27

3-
* use secure random number generator for frame masking.
8+
- use secure random number generator for frame masking.
49

510
## 2.4.4
611

7-
* Require Dart `^3.3`
8-
* Require `package:web` `^0.5.0`.
12+
- Require Dart `^3.3`
13+
- Require `package:web` `^0.5.0`.
914

1015
## 2.4.3
1116

pkgs/web_socket_channel/lib/src/_connect_api.dart

Lines changed: 0 additions & 15 deletions
This file was deleted.

pkgs/web_socket_channel/lib/src/_connect_html.dart

Lines changed: 0 additions & 16 deletions
This file was deleted.

pkgs/web_socket_channel/lib/src/_connect_io.dart

Lines changed: 0 additions & 15 deletions
This file was deleted.

pkgs/web_socket_channel/lib/src/channel.dart

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,7 @@ import 'package:async/async.dart';
99
import 'package:crypto/crypto.dart';
1010
import 'package:stream_channel/stream_channel.dart';
1111

12-
import '_connect_api.dart'
13-
if (dart.library.io) '_connect_io.dart'
14-
if (dart.library.js_interop) '_connect_html.dart' as platform;
12+
import '../web_socket_adapter_web_socket_channel.dart';
1513
import 'copy/web_socket_impl.dart';
1614
import 'exception.dart';
1715

@@ -141,7 +139,7 @@ class WebSocketChannel extends StreamChannelMixin {
141139
/// If there are errors creating the connection the [ready] future will
142140
/// complete with an error.
143141
factory WebSocketChannel.connect(Uri uri, {Iterable<String>? protocols}) =>
144-
platform.connect(uri, protocols: protocols);
142+
WebSocketAdapterWebSocketChannel.connect(uri, protocols: protocols);
145143
}
146144

147145
/// The sink exposed by a [WebSocketChannel].
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:async';
6+
import 'dart:typed_data';
7+
8+
import 'package:async/async.dart';
9+
import 'package:stream_channel/stream_channel.dart';
10+
import 'package:web_socket/web_socket.dart';
11+
12+
import 'src/channel.dart';
13+
import 'src/exception.dart';
14+
15+
/// A [WebSocketChannel] implemented using [WebSocket].
16+
class WebSocketAdapterWebSocketChannel extends StreamChannelMixin
17+
implements WebSocketChannel {
18+
@override
19+
String? get protocol => _protocol;
20+
String? _protocol;
21+
22+
@override
23+
int? get closeCode => _closeCode;
24+
int? _closeCode;
25+
26+
@override
27+
String? get closeReason => _closeReason;
28+
String? _closeReason;
29+
30+
/// The close code set by the local user.
31+
///
32+
/// To ensure proper ordering, this is stored until we get a done event on
33+
/// [_controller.local.stream].
34+
int? _localCloseCode;
35+
36+
/// The close reason set by the local user.
37+
///
38+
/// To ensure proper ordering, this is stored until we get a done event on
39+
/// [_controller.local.stream].
40+
String? _localCloseReason;
41+
42+
/// Completer for [ready].
43+
final _readyCompleter = Completer<void>();
44+
45+
@override
46+
Future<void> get ready => _readyCompleter.future;
47+
48+
@override
49+
Stream get stream => _controller.foreign.stream;
50+
51+
final _controller =
52+
StreamChannelController<Object?>(sync: true, allowForeignErrors: false);
53+
54+
@override
55+
late final WebSocketSink sink = _WebSocketSink(this);
56+
57+
/// Creates a new WebSocket connection.
58+
///
59+
/// If provided, the [protocols] argument indicates that subprotocols that
60+
/// the peer is able to select. See
61+
/// [RFC-6455 1.9](https://datatracker.ietf.org/doc/html/rfc6455#section-1.9).
62+
///
63+
/// After construction, the [WebSocketAdapterWebSocketChannel] may not be
64+
/// connected to the peer. The [ready] future will complete after the channel
65+
/// is connected. If there are errors creating the connection the [ready]
66+
/// future will complete with an error.
67+
factory WebSocketAdapterWebSocketChannel.connect(Uri url,
68+
{Iterable<String>? protocols}) =>
69+
WebSocketAdapterWebSocketChannel._(
70+
WebSocket.connect(url, protocols: protocols));
71+
72+
// Create a [WebSocketWebSocketChannelAdapter] from an existing [WebSocket].
73+
factory WebSocketAdapterWebSocketChannel.fromWebSocket(WebSocket webSocket) =>
74+
WebSocketAdapterWebSocketChannel._(Future.value(webSocket));
75+
76+
WebSocketAdapterWebSocketChannel._(Future<WebSocket> webSocketFuture) {
77+
webSocketFuture.then((webSocket) {
78+
var remoteClosed = false;
79+
webSocket.events.listen((event) {
80+
switch (event) {
81+
case TextDataReceived(text: final text):
82+
_controller.local.sink.add(text);
83+
case BinaryDataReceived(data: final data):
84+
_controller.local.sink.add(data);
85+
case CloseReceived(code: final code, reason: final reason):
86+
remoteClosed = true;
87+
_closeCode = code;
88+
_closeReason = reason;
89+
_controller.local.sink.close();
90+
}
91+
});
92+
_controller.local.stream.listen((obj) {
93+
try {
94+
switch (obj) {
95+
case final String s:
96+
webSocket.sendText(s);
97+
case final Uint8List b:
98+
webSocket.sendBytes(b);
99+
case final List<int> b:
100+
webSocket.sendBytes(Uint8List.fromList(b));
101+
default:
102+
throw UnsupportedError('Cannot send ${obj.runtimeType}');
103+
}
104+
} on WebSocketConnectionClosed catch (_) {
105+
// There is nowhere to surface this error; `_controller.local.sink`
106+
// has already been closed.
107+
}
108+
}, onDone: () {
109+
if (!remoteClosed) {
110+
webSocket.close(_localCloseCode, _localCloseReason);
111+
}
112+
});
113+
_protocol = webSocket.protocol;
114+
_readyCompleter.complete();
115+
}, onError: (Object e) {
116+
_readyCompleter.completeError(WebSocketChannelException.from(e));
117+
});
118+
}
119+
}
120+
121+
/// A [WebSocketSink] that tracks the close code and reason passed to [close].
122+
class _WebSocketSink extends DelegatingStreamSink implements WebSocketSink {
123+
/// The channel to which this sink belongs.
124+
final WebSocketAdapterWebSocketChannel _channel;
125+
126+
_WebSocketSink(WebSocketAdapterWebSocketChannel channel)
127+
: _channel = channel,
128+
super(channel._controller.foreign.sink);
129+
130+
@override
131+
Future close([int? closeCode, String? closeReason]) {
132+
_channel._localCloseCode = closeCode;
133+
_channel._localCloseReason = closeReason;
134+
return super.close();
135+
}
136+
}

pkgs/web_socket_channel/pubspec.yaml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: web_socket_channel
2-
version: 2.4.5
2+
version: 3.0.0-wip
33
description: >-
44
StreamChannel wrappers for WebSockets. Provides a cross-platform
55
WebSocketChannel API, a cross-platform implementation of that API that
@@ -14,7 +14,14 @@ dependencies:
1414
crypto: ^3.0.0
1515
stream_channel: ^2.1.0
1616
web: ^0.5.0
17+
web_socket: ^0.1.0
1718

1819
dev_dependencies:
1920
dart_flutter_team_lints: ^2.0.0
2021
test: ^1.16.0
22+
23+
# Remove this when versions of `package:test` and `shelf_web_socket` that support
24+
# channel_web_socket 3.0 are released.
25+
dependency_overrides:
26+
shelf_web_socket: 1.0.4
27+
test: 1.25.2
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:async';
6+
import 'dart:io';
7+
8+
import 'package:stream_channel/stream_channel.dart';
9+
10+
Future<void> hybridMain(StreamChannel<Object?> channel) async {
11+
late HttpServer server;
12+
13+
server = (await HttpServer.bind('localhost', 0))
14+
..transform(WebSocketTransformer())
15+
.listen((WebSocket webSocket) => webSocket.listen((data) {
16+
if (data == 'close') {
17+
webSocket.close(3001, 'you asked me to');
18+
} else {
19+
webSocket.add(data);
20+
}
21+
}));
22+
23+
channel.sink.add(server.port);
24+
await channel
25+
.stream.first; // Any writes indicates that the server should exit.
26+
unawaited(server.close());
27+
}
28+
29+
/// Starts an WebSocket server that echos the payload of the request.
30+
Future<StreamChannel<Object?>> startServer() async {
31+
final controller = StreamChannelController<Object?>(sync: true);
32+
unawaited(hybridMain(controller.foreign));
33+
return controller.local;
34+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:stream_channel/stream_channel.dart';
6+
import 'package:test/test.dart';
7+
8+
/// Starts an WebSocket server that echos the payload of the request.
9+
/// Copied from `echo_server_vm.dart`.
10+
Future<StreamChannel<Object?>> startServer() async => spawnHybridCode(r'''
11+
import 'dart:async';
12+
import 'dart:io';
13+
14+
import 'package:stream_channel/stream_channel.dart';
15+
16+
/// Starts an WebSocket server that echos the payload of the request.
17+
Future<void> hybridMain(StreamChannel<Object?> channel) async {
18+
late HttpServer server;
19+
20+
server = (await HttpServer.bind('localhost', 0))
21+
..transform(WebSocketTransformer())
22+
.listen((WebSocket webSocket) => webSocket.listen((data) {
23+
if (data == 'close') {
24+
webSocket.close(3001, 'you asked me to');
25+
} else {
26+
webSocket.add(data);
27+
}
28+
}));
29+
30+
channel.sink.add(server.port);
31+
await channel
32+
.stream.first; // Any writes indicates that the server should exit.
33+
unawaited(server.close());
34+
}
35+
''');

pkgs/web_socket_channel/test/io_test.dart

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ void main() {
2424
channel.stream.listen((request) {
2525
expect(request, equals('ping'));
2626
channel.sink.add('pong');
27-
channel.sink.close(5678, 'raisin');
27+
channel.sink.close(3678, 'raisin');
2828
});
2929
});
3030

@@ -45,7 +45,7 @@ void main() {
4545
}
4646
n++;
4747
}, onDone: expectAsync0(() {
48-
expect(channel.closeCode, equals(5678));
48+
expect(channel.closeCode, equals(3678));
4949
expect(channel.closeReason, equals('raisin'));
5050
}));
5151
});
@@ -70,7 +70,7 @@ void main() {
7070
channel.stream.listen(
7171
expectAsync1((message) {
7272
expect(message, equals('pong'));
73-
channel.sink.close(5678, 'raisin');
73+
channel.sink.close(3678, 'raisin');
7474
}, count: 1),
7575
onDone: expectAsync0(() {}));
7676
});
@@ -97,7 +97,7 @@ void main() {
9797
channel.stream.listen(
9898
expectAsync1((message) {
9999
expect(message, equals('pong'));
100-
channel.sink.close(5678, 'raisin');
100+
channel.sink.close(3678, 'raisin');
101101
}, count: 1),
102102
onDone: expectAsync0(() {}));
103103
});
@@ -109,7 +109,7 @@ void main() {
109109
expect(() async {
110110
final channel = IOWebSocketChannel(webSocket);
111111
await channel.stream.drain<void>();
112-
expect(channel.closeCode, equals(5678));
112+
expect(channel.closeCode, equals(3678));
113113
expect(channel.closeReason, equals('raisin'));
114114
}(), completes);
115115
});
@@ -118,7 +118,7 @@ void main() {
118118

119119
expect(channel.ready, completes);
120120

121-
await channel.sink.close(5678, 'raisin');
121+
await channel.sink.close(3678, 'raisin');
122122
});
123123

124124
test('.connect wraps a connection error in WebSocketChannelException',
@@ -192,7 +192,7 @@ void main() {
192192
expect(() async {
193193
final channel = IOWebSocketChannel(webSocket);
194194
await channel.stream.drain<void>();
195-
expect(channel.closeCode, equals(5678));
195+
expect(channel.closeCode, equals(3678));
196196
expect(channel.closeReason, equals('raisin'));
197197
}(), completes);
198198
});
@@ -202,7 +202,7 @@ void main() {
202202
connectTimeout: const Duration(milliseconds: 1000),
203203
);
204204
expect(channel.ready, completes);
205-
await channel.sink.close(5678, 'raisin');
205+
await channel.sink.close(3678, 'raisin');
206206
});
207207

208208
test('.respects timeout parameter when trying to connect', () async {

0 commit comments

Comments
 (0)