Merge pull request #1420 from dart-lang/merge-web_socket_channel-package
Merge `package:web_socket_channel`
diff --git a/.github/ISSUE_TEMPLATE/web_socket_channel.md b/.github/ISSUE_TEMPLATE/web_socket_channel.md
new file mode 100644
index 0000000..9f0cfc1
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/web_socket_channel.md
@@ -0,0 +1,5 @@
+---
+name: "package:web_socket_channel"
+about: "Create a bug or file a feature request against package:web_socket_channel."
+labels: "package:web_socket_channel"
+---
\ No newline at end of file
diff --git a/.github/labeler.yml b/.github/labeler.yml
index 575d4f0..1bfe963 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -32,6 +32,10 @@
- changed-files:
- any-glob-to-any-file: 'pkgs/http_parser/**'
+'package:web_socket_channel':
+ - changed-files:
+ - any-glob-to-any-file: 'pkgs/web_socket_channel/**'
+
'package:web_socket_conformance_tests':
- changed-files:
- - any-glob-to-any-file: 'pkgs/web_socket_conformance_tests/**'
+ - any-glob-to-any-file: 'pkgs/http_client_conformance_tests/**'
diff --git a/.github/workflows/web_socket_channel.yaml b/.github/workflows/web_socket_channel.yaml
new file mode 100644
index 0000000..ecc98b8
--- /dev/null
+++ b/.github/workflows/web_socket_channel.yaml
@@ -0,0 +1,80 @@
+name: package:web_socket_channel
+
+on:
+ push:
+ branches:
+ - master
+ paths:
+ - '.github/workflows/web_socket_channel.yaml'
+ - 'pkgs/web_socket_channel/**'
+ pull_request:
+ paths:
+ - '.github/workflows/web_socket_channel.yaml'
+ - 'pkgs/web_socket_channel/**'
+ schedule:
+ - cron: "0 0 * * 0"
+
+defaults:
+ run:
+ working-directory: pkgs/web_socket_channel/
+
+env:
+ PUB_ENVIRONMENT: bot.github
+
+jobs:
+ # Check code formatting and static analysis on a single OS (linux)
+ # against Dart dev.
+ analyze:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ sdk: [dev]
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+ with:
+ sdk: ${{ matrix.sdk }}
+ - id: install
+ name: Install dependencies
+ run: dart pub get
+ - name: Check formatting
+ run: dart format --output=none --set-exit-if-changed .
+ if: always() && steps.install.outcome == 'success'
+ - name: Analyze code
+ run: dart analyze
+ if: always() && steps.install.outcome == 'success'
+
+ test:
+ needs: analyze
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest]
+ sdk: [3.3, dev]
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+ with:
+ sdk: ${{ matrix.sdk }}
+ - id: install
+ name: Install dependencies
+ run: dart pub get
+ - name: Run VM tests
+ run: dart test --platform vm
+ if: always() && steps.install.outcome == 'success'
+ - name: Run Chrome tests
+ run: dart test --platform chrome
+ if: always() && steps.install.outcome == 'success'
+ - name: Run Chrome tests - wasm
+ run: dart test --platform chrome --compiler dart2wasm
+ if: always() && steps.install.outcome == 'success' && matrix.sdk == 'dev'
+
+ # Run analysis against the oldest supported pub constraints.
+ downgrade:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+ - run: dart pub downgrade
+ - run: dart analyze
diff --git a/README.md b/README.md
index fac1ade..78127a7 100644
--- a/README.md
+++ b/README.md
@@ -21,6 +21,7 @@
| [http_profile](pkgs/http_profile/) | A library used by HTTP client authors to integrate with the DevTools Network View. | [](https://pub.dev/packages/http_profile) |
| [ok_http](pkgs/ok_http/) | An Android Flutter plugin that provides access to the [OkHttp](https://square.github.io/okhttp/) HTTP client and the OkHttp [WebSocket](https://square.github.io/okhttp/5.x/okhttp/okhttp3/-web-socket/index.html) API. | [](https://pub.dev/packages/ok_http) |
| [web_socket](pkgs/web_socket/) | Any easy-to-use library for communicating with WebSockets that has multiple implementations. | [](https://pub.dev/packages/web_socket) |
+| [web_socket_channel](pkgs/web_socket_channel/) | StreamChannel wrappers for WebSockets. Provides a cross-platform WebSocketChannel API. | [](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Aweb_socket_channel) | [](https://pub.dev/packages/web_socket_channel) |
| [web_socket_conformance_tests](pkgs/web_socket_conformance_tests/) | A library that tests whether implementations of `package:web_socket`'s `WebSocket` class behave as expected. | |
## Contributing
diff --git a/pkgs/web_socket_channel/.gitignore b/pkgs/web_socket_channel/.gitignore
new file mode 100644
index 0000000..49ce72d
--- /dev/null
+++ b/pkgs/web_socket_channel/.gitignore
@@ -0,0 +1,3 @@
+.dart_tool/
+.packages
+pubspec.lock
diff --git a/pkgs/web_socket_channel/CHANGELOG.md b/pkgs/web_socket_channel/CHANGELOG.md
new file mode 100644
index 0000000..4677fbf
--- /dev/null
+++ b/pkgs/web_socket_channel/CHANGELOG.md
@@ -0,0 +1,160 @@
+## 3.0.2
+
+- Move to `dart-lang/http` monorepo.
+
+## 3.0.1
+
+- Remove unnecessary `dependency_overrides`.
+- Remove obsolete documentation for `WebSocketChannel.new`.
+- Update package `web: '>=0.5.0 <2.0.0'`.
+
+## 3.0.0
+
+- Provide an adapter around `package:web_socket` `WebSocket`s and make it the
+ default implementation for `WebSocketChannel.connect`.
+- **BREAKING**: Remove `WebSocketChannel` constructor.
+- **BREAKING**: Make `WebSocketChannel` an `abstract interface`.
+- **BREAKING**: `IOWebSocketChannel.ready` will throw
+ `WebSocketChannelException` instead of `WebSocketException`.
+
+## 2.4.5
+
+- use secure random number generator for frame masking.
+
+## 2.4.4
+
+- Require Dart `^3.3`
+- Require `package:web` `^0.5.0`.
+
+## 2.4.3
+
+- `HtmlWebSocketChannel`: Relax the type of the websocket parameter to the
+ constructor in order to mitigate a breaking change introduced in `2.4.1`.
+
+## 2.4.2 (retracted)
+
+- Allow `web: '>=0.3.0 <0.5.0'`
+
+## 2.4.1
+
+- Update the examples to use `WebSocketChannel.ready` and clarify that
+ `WebSocketChannel.ready` should be awaited before sending data over the
+ `WebSocketChannel`.
+- Mention `ready` in the docs for `connect`.
+- Bump minimum Dart version to 3.2.0
+- Move to `pkg:web` to support WebAssembly compilation.
+
+## 2.4.0
+
+- Add a `customClient` parameter to the `IOWebSocketChannel.connect` factory,
+ which allows the user to provide a custom `HttpClient` instance to use for the
+ WebSocket connection
+- Bump minimum Dart version to 2.15.0
+
+## 2.3.0
+
+- Added a Future `ready` property to `WebSocketChannel`, which completes when
+ the connection is established
+- Added a `connectTimeout` parameter to the `IOWebSocketChannel.connect` factory,
+ which controls the timeout of the WebSocket Future.
+- Use platform agnostic code in README example.
+
+## 2.2.0
+
+- Add `HtmlWebSocketChannel.innerWebSocket` getter to access features not exposed
+ through the shared `WebSocketChannel` interface.
+
+## 2.1.0
+
+- Add `IOWebSocketChannel.innerWebSocket` getter to access features not exposed
+ through the shared `WebSocketChannel` interface.
+
+## 2.0.0
+
+- Support null safety.
+- Require Dart 2.12.
+
+## 1.2.0
+
+* Add `protocols` argument to `WebSocketChannel.connect`. See the docs for
+ `WebSocket.connet`.
+* Allow the latest crypto release (`3.x`).
+
+## 1.1.0
+
+* Add `WebSocketChannel.connect` factory constructor supporting platform
+ independent creation of WebSockets providing the lowest common denominator
+ of support on dart:io and dart:html.
+
+## 1.0.15
+
+* bug fix don't pass protocols parameter to WebSocket.
+
+## 1.0.14
+
+* Updates to handle `Socket implements Stream<Uint8List>`
+
+## 1.0.13
+
+* Internal changes for consistency with the Dart SDK.
+
+## 1.0.12
+
+* Allow `stream_channel` version 2.x
+
+## 1.0.11
+
+* Fixed description in pubspec.
+
+* Fixed lints in README.md.
+
+## 1.0.10
+
+* Fixed links in README.md.
+
+* Added an example.
+
+* Fixed analysis lints that affected package score.
+
+## 1.0.9
+
+* Set max SDK version to `<3.0.0`.
+
+## 1.0.8
+
+* Remove use of deprecated constant name.
+
+## 1.0.7
+
+* Support the latest dev SDK.
+
+## 1.0.6
+
+* Declare support for `async` 2.0.0.
+
+## 1.0.5
+
+* Increase the SDK version constraint to `<2.0.0-dev.infinity`.
+
+## 1.0.4
+
+* Support `crypto` 2.0.0.
+
+## 1.0.3
+
+* Fix all strong-mode errors and warnings.
+
+* Fix a bug where `HtmlWebSocketChannel.close()` would crash on non-Dartium
+ browsers if the close code and reason weren't provided explicitly.
+
+## 1.0.2
+
+* Properly use `BASE64` from `dart:convert` rather than `crypto`.
+
+## 1.0.1
+
+* Add support for `crypto` 1.0.0.
+
+## 1.0.0
+
+* Initial version
diff --git a/pkgs/web_socket_channel/LICENSE b/pkgs/web_socket_channel/LICENSE
new file mode 100644
index 0000000..2372431
--- /dev/null
+++ b/pkgs/web_socket_channel/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2016, the Dart project authors.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+ * Neither the name of Google LLC nor the names of its
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/pkgs/web_socket_channel/README.md b/pkgs/web_socket_channel/README.md
new file mode 100644
index 0000000..39db951
--- /dev/null
+++ b/pkgs/web_socket_channel/README.md
@@ -0,0 +1,72 @@
+[](https://pub.dev/packages/web_socket_channel)
+[](https://pub.dev/packages/web_socket_channel/publisher)
+
+`package:web_socket_channel` provides cross-platform
+[`StreamChannel`][stream_channel] wrappers for WebSocket connections.
+
+## Docs and Usage
+
+It provides a cross-platform
+[`WebSocketChannel`][WebSocketChannel] API, a cross-platform implementation of
+that API that communicates over an underlying [`StreamChannel`][stream_channel],
+[an implementation][IOWebSocketChannel] that wraps `dart:io`'s `WebSocket`
+class, and [a similar implementation][HtmlWebSocketChannel] that wraps
+`dart:html`'s.
+
+[stream_channel]: https://pub.dev/packages/stream_channel
+[WebSocketChannel]: https://pub.dev/documentation/web_socket_channel/latest/web_socket_channel/WebSocketChannel-class.html
+[IOWebSocketChannel]: https://pub.dev/documentation/web_socket_channel/latest/web_socket_channel.io/IOWebSocketChannel-class.html
+[HtmlWebSocketChannel]: https://pub.dev/documentation/web_socket_channel/latest/web_socket_channel.html/HtmlWebSocketChannel-class.html
+
+It also provides constants for the WebSocket protocol's pre-defined status codes
+in the [`status.dart` library][status]. It's strongly recommended that users
+import this library with the prefix `status`.
+
+[status]: https://pub.dev/documentation/web_socket_channel/latest/status/status-library.html
+
+```dart
+import 'package:web_socket_channel/web_socket_channel.dart';
+import 'package:web_socket_channel/status.dart' as status;
+
+main() async {
+ final wsUrl = Uri.parse('ws://example.com');
+ final channel = WebSocketChannel.connect(wsUrl);
+
+ await channel.ready;
+
+ channel.stream.listen((message) {
+ channel.sink.add('received!');
+ channel.sink.close(status.goingAway);
+ });
+}
+```
+
+## `WebSocketChannel`
+
+The [`WebSocketChannel`][WebSocketChannel] class's most important role is as the
+interface for WebSocket stream channels across all implementations and all
+platforms. In addition to the base `StreamChannel` interface, it adds a
+[`protocol`][protocol] getter that returns the negotiated protocol for the
+socket, as well as [`closeCode`][closeCode] and [`closeReason`][closeReason]
+getters that provide information about why the socket closed.
+
+[protocol]: https://pub.dev/documentation/web_socket_channel/latest/web_socket_channel/WebSocketChannel/protocol.html
+[closeCode]: https://pub.dev/documentation/web_socket_channel/latest/web_socket_channel/WebSocketChannel/closeCode.html
+[closeReason]: https://pub.dev/documentation/web_socket_channel/latest/web_socket_channel/WebSocketChannel/closeReason.html
+
+The channel's [`sink` property][sink] is also special. It returns a
+[`WebSocketSink`][WebSocketSink], which is just like a `StreamSink` except that
+its [`close()`][sink.close] method supports optional `closeCode` and
+`closeReason` parameters. These parameters allow the caller to signal to the
+other socket exactly why they're closing the connection.
+
+[sink]: https://pub.dev/documentation/web_socket_channel/latest/web_socket_channel/WebSocketChannel/sink.html
+[WebSocketSink]: https://pub.dev/documentation/web_socket_channel/latest/web_socket_channel/WebSocketSink-class.html
+[sink.close]: https://pub.dev/documentation/web_socket_channel/latest/web_socket_channel/WebSocketSink/close.html
+
+`WebSocketChannel` also works as a cross-platform implementation of the
+WebSocket protocol. The [`WebSocketChannel.connect` constructor][connect]
+connects to a listening server using the appropriate implementation for the
+platform.
+
+[connect]: https://pub.dev/documentation/web_socket_channel/latest/web_socket_channel/WebSocketChannel/WebSocketChannel.connect.html
diff --git a/pkgs/web_socket_channel/analysis_options.yaml b/pkgs/web_socket_channel/analysis_options.yaml
new file mode 100644
index 0000000..b7c530a
--- /dev/null
+++ b/pkgs/web_socket_channel/analysis_options.yaml
@@ -0,0 +1,27 @@
+include: package:dart_flutter_team_lints/analysis_options.yaml
+
+analyzer:
+ language:
+ strict-casts: true
+
+linter:
+ rules:
+ - avoid_bool_literals_in_conditional_expressions
+ - avoid_classes_with_only_static_members
+ - avoid_private_typedef_functions
+ - avoid_redundant_argument_values
+ - avoid_returning_this
+ - avoid_unused_constructor_parameters
+ - avoid_void_async
+ - cancel_subscriptions
+ - join_return_with_assignment
+ - literal_only_boolean_expressions
+ - missing_whitespace_between_adjacent_strings
+ - no_adjacent_strings_in_list
+ - no_runtimeType_toString
+ - prefer_const_declarations
+ - prefer_expression_function_bodies
+ - prefer_final_locals
+ - prefer_void_to_null
+ - unnecessary_await_in_return
+ - use_string_buffers
diff --git a/pkgs/web_socket_channel/example/example.dart b/pkgs/web_socket_channel/example/example.dart
new file mode 100644
index 0000000..b09b798
--- /dev/null
+++ b/pkgs/web_socket_channel/example/example.dart
@@ -0,0 +1,18 @@
+// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:web_socket_channel/status.dart' as status;
+import 'package:web_socket_channel/web_socket_channel.dart';
+
+void main() async {
+ final wsUrl = Uri.parse('ws://example.com');
+ final channel = WebSocketChannel.connect(wsUrl);
+
+ await channel.ready;
+
+ channel.stream.listen((message) {
+ channel.sink.add('received!');
+ channel.sink.close(status.goingAway);
+ });
+}
diff --git a/pkgs/web_socket_channel/lib/adapter_web_socket_channel.dart b/pkgs/web_socket_channel/lib/adapter_web_socket_channel.dart
new file mode 100644
index 0000000..0271f44
--- /dev/null
+++ b/pkgs/web_socket_channel/lib/adapter_web_socket_channel.dart
@@ -0,0 +1,149 @@
+// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:typed_data';
+
+import 'package:async/async.dart';
+import 'package:stream_channel/stream_channel.dart';
+import 'package:web_socket/web_socket.dart';
+
+import 'src/channel.dart';
+import 'src/exception.dart';
+
+/// A [WebSocketChannel] implemented using [WebSocket].
+class AdapterWebSocketChannel extends StreamChannelMixin
+ implements WebSocketChannel {
+ @override
+ String? get protocol => _protocol;
+ String? _protocol;
+
+ @override
+ int? get closeCode => _closeCode;
+ int? _closeCode;
+
+ @override
+ String? get closeReason => _closeReason;
+ String? _closeReason;
+
+ /// The close code set by the local user.
+ ///
+ /// To ensure proper ordering, this is stored until we get a done event on
+ /// [StreamChannelController.local]`.stream`.
+ int? _localCloseCode;
+
+ /// The close reason set by the local user.
+ ///
+ /// To ensure proper ordering, this is stored until we get a done event on
+ /// [StreamChannelController.local]`.stream`.
+ String? _localCloseReason;
+
+ /// Completer for [ready].
+ final _readyCompleter = Completer<void>();
+
+ @override
+ Future<void> get ready => _readyCompleter.future;
+
+ @override
+ Stream get stream => _controller.foreign.stream;
+
+ final _controller =
+ StreamChannelController<Object?>(sync: true, allowForeignErrors: false);
+
+ @override
+ late final WebSocketSink sink = _WebSocketSink(this);
+
+ /// Creates a new WebSocket connection.
+ ///
+ /// If provided, the [protocols] argument indicates that subprotocols that
+ /// the peer is able to select. See
+ /// [RFC-6455 1.9](https://datatracker.ietf.org/doc/html/rfc6455#section-1.9).
+ ///
+ /// After construction, the [AdapterWebSocketChannel] may not be
+ /// connected to the peer. The [ready] future will complete after the channel
+ /// is connected. If there are errors creating the connection the [ready]
+ /// future will complete with an error.
+ factory AdapterWebSocketChannel.connect(Uri url,
+ {Iterable<String>? protocols}) =>
+ AdapterWebSocketChannel(WebSocket.connect(url, protocols: protocols));
+
+ // Construct a [WebSocketWebSocketChannelAdapter] from an existing
+ // [WebSocket].
+ AdapterWebSocketChannel(FutureOr<WebSocket> webSocket) {
+ Future<WebSocket> webSocketFuture;
+ if (webSocket is WebSocket) {
+ webSocketFuture = Future.value(webSocket);
+ } else {
+ webSocketFuture = webSocket;
+ }
+
+ webSocketFuture.then((webSocket) {
+ webSocket.events.listen((event) {
+ switch (event) {
+ case TextDataReceived(text: final text):
+ _controller.local.sink.add(text);
+ case BinaryDataReceived(data: final data):
+ _controller.local.sink.add(data);
+ case CloseReceived(code: final code, reason: final reason):
+ _closeCode = code;
+ _closeReason = reason;
+ _controller.local.sink.close();
+ }
+ });
+ _controller.local.stream.listen((obj) {
+ try {
+ switch (obj) {
+ case final String s:
+ webSocket.sendText(s);
+ case final Uint8List b:
+ webSocket.sendBytes(b);
+ case final List<int> b:
+ webSocket.sendBytes(Uint8List.fromList(b));
+ default:
+ throw UnsupportedError('Cannot send ${obj.runtimeType}');
+ }
+ } on WebSocketConnectionClosed {
+ // There is nowhere to surface this error; `_controller.local.sink`
+ // has already been closed.
+ }
+ }, onDone: () async {
+ try {
+ await webSocket.close(_localCloseCode, _localCloseReason);
+ } on WebSocketConnectionClosed {
+ // It is not an error to close an already-closed `WebSocketChannel`.
+ }
+ });
+ _protocol = webSocket.protocol;
+ _readyCompleter.complete();
+ }, onError: (Object e) {
+ Exception error;
+ if (e is TimeoutException) {
+ // Required for backwards compatibility with `IOWebSocketChannel`.
+ error = e;
+ } else {
+ error = WebSocketChannelException.from(e);
+ }
+ _readyCompleter.completeError(error);
+ _controller.local.sink.addError(error);
+ _controller.local.sink.close();
+ });
+ }
+}
+
+/// A [WebSocketSink] that tracks the close code and reason passed to [close].
+class _WebSocketSink extends DelegatingStreamSink implements WebSocketSink {
+ /// The channel to which this sink belongs.
+ final AdapterWebSocketChannel _channel;
+
+ _WebSocketSink(AdapterWebSocketChannel channel)
+ : _channel = channel,
+ super(channel._controller.foreign.sink);
+
+ @override
+ Future close([int? closeCode, String? closeReason]) {
+ _channel._localCloseCode = closeCode;
+ _channel._localCloseReason = closeReason;
+ return super.close();
+ }
+}
diff --git a/pkgs/web_socket_channel/lib/html.dart b/pkgs/web_socket_channel/lib/html.dart
new file mode 100644
index 0000000..bc99a63
--- /dev/null
+++ b/pkgs/web_socket_channel/lib/html.dart
@@ -0,0 +1,203 @@
+// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:js_interop';
+import 'dart:typed_data';
+
+import 'package:async/async.dart';
+import 'package:stream_channel/stream_channel.dart';
+import 'package:web/web.dart';
+
+import 'src/channel.dart';
+import 'src/exception.dart';
+
+/// A [WebSocketChannel] that communicates using a `dart:html` [WebSocket].
+class HtmlWebSocketChannel extends StreamChannelMixin
+ implements WebSocketChannel {
+ /// The underlying `dart:html` [WebSocket].
+ final WebSocket innerWebSocket;
+
+ @override
+ String? get protocol => innerWebSocket.protocol;
+
+ @override
+ int? get closeCode => _closeCode;
+ int? _closeCode;
+
+ @override
+ String? get closeReason => _closeReason;
+ String? _closeReason;
+
+ /// The number of bytes of data that have been queued but not yet transmitted
+ /// to the network.
+ int? get bufferedAmount => innerWebSocket.bufferedAmount;
+
+ /// The close code set by the local user.
+ ///
+ /// To ensure proper ordering, this is stored until we get a done event on
+ /// [StreamChannelController.local]`.stream`.
+ int? _localCloseCode;
+
+ /// The close reason set by the local user.
+ ///
+ /// To ensure proper ordering, this is stored until we get a done event on
+ /// [StreamChannelController.local]`.stream`.
+ String? _localCloseReason;
+
+ /// Completer for [ready].
+ late Completer<void> _readyCompleter;
+
+ @override
+ Future<void> get ready => _readyCompleter.future;
+
+ @override
+ Stream get stream => _controller.foreign.stream;
+
+ final _controller =
+ StreamChannelController<Object?>(sync: true, allowForeignErrors: false);
+
+ @override
+ late final WebSocketSink sink = _HtmlWebSocketSink(this);
+
+ /// Creates a new WebSocket connection.
+ ///
+ /// Connects to [url] using [WebSocket.new] and returns a channel that can be
+ /// used to communicate over the resulting socket. The [url] may be either a
+ /// [String] or a [Uri]. The [protocols] parameter is the same as for
+ /// [WebSocket.new].
+ ///
+ /// The [binaryType] parameter controls what type is used for binary messages
+ /// received by this socket. It defaults to [BinaryType.list], which causes
+ /// binary messages to be delivered as [Uint8List]s. If it's
+ /// [BinaryType.blob], they're delivered as [Blob]s instead.
+ HtmlWebSocketChannel.connect(Object url,
+ {Iterable<String>? protocols, BinaryType? binaryType})
+ : this(
+ WebSocket(
+ url.toString(),
+ protocols?.map((e) => e.toJS).toList().toJS ?? JSArray(),
+ )..binaryType = (binaryType ?? BinaryType.list).value,
+ );
+
+ /// Creates a channel wrapping [webSocket].
+ ///
+ /// The parameter [webSocket] should be either a dart:html `WebSocket`
+ /// instance or a package:web [WebSocket] instance.
+ HtmlWebSocketChannel(Object /*WebSocket*/ webSocket)
+ : innerWebSocket = webSocket as WebSocket {
+ _readyCompleter = Completer();
+ if (innerWebSocket.readyState == WebSocket.OPEN) {
+ _readyCompleter.complete();
+ _listen();
+ } else {
+ if (innerWebSocket.readyState == WebSocket.CLOSING ||
+ innerWebSocket.readyState == WebSocket.CLOSED) {
+ _readyCompleter.completeError(WebSocketChannelException(
+ 'WebSocket state error: ${innerWebSocket.readyState}'));
+ }
+ // The socket API guarantees that only a single open event will be
+ // emitted.
+ innerWebSocket.onOpen.first.then((_) {
+ _readyCompleter.complete();
+ _listen();
+ });
+ }
+
+ // The socket API guarantees that only a single error event will be emitted,
+ // and that once it is no open or message events will be emitted.
+ innerWebSocket.onError.first.then((_) {
+ // Unfortunately, the underlying WebSocket API doesn't expose any
+ // specific information about the error itself.
+ final error = WebSocketChannelException('WebSocket connection failed.');
+ if (!_readyCompleter.isCompleted) {
+ _readyCompleter.completeError(error);
+ }
+ _controller.local.sink.addError(error);
+ _controller.local.sink.close();
+ });
+
+ innerWebSocket.onMessage.listen(_innerListen);
+
+ // The socket API guarantees that only a single error event will be emitted,
+ // and that once it is no other events will be emitted.
+ innerWebSocket.onClose.first.then((event) {
+ _closeCode = event.code;
+ _closeReason = event.reason;
+ _controller.local.sink.close();
+ });
+ }
+
+ void _innerListen(MessageEvent event) {
+ // Event data will be ArrayBuffer, Blob, or String.
+ final eventData = event.data;
+ final Object? data;
+ if (eventData.typeofEquals('string')) {
+ data = (eventData as JSString).toDart;
+ } else if (eventData.typeofEquals('object') &&
+ (eventData as JSObject).instanceOfString('ArrayBuffer')) {
+ data = (eventData as JSArrayBuffer).toDart.asUint8List();
+ } else {
+ // Blobs are passed directly.
+ data = eventData;
+ }
+ _controller.local.sink.add(data);
+ }
+
+ /// Pipes user events to [innerWebSocket].
+ void _listen() {
+ _controller.local.stream.listen((obj) => innerWebSocket.send(obj!.jsify()!),
+ onDone: () {
+ // On Chrome and possibly other browsers, `null` can't be passed as the
+ // default here. The actual arity of the function call must be correct or
+ // it will fail.
+ if ((_localCloseCode, _localCloseReason)
+ case (final closeCode?, final closeReason?)) {
+ innerWebSocket.close(closeCode, closeReason);
+ } else if (_localCloseCode case final closeCode?) {
+ innerWebSocket.close(closeCode);
+ } else {
+ innerWebSocket.close();
+ }
+ });
+ }
+}
+
+/// A [WebSocketSink] that tracks the close code and reason passed to [close].
+class _HtmlWebSocketSink extends DelegatingStreamSink implements WebSocketSink {
+ /// The channel to which this sink belongs.
+ final HtmlWebSocketChannel _channel;
+
+ _HtmlWebSocketSink(HtmlWebSocketChannel channel)
+ : _channel = channel,
+ super(channel._controller.foreign.sink);
+
+ @override
+ Future close([int? closeCode, String? closeReason]) {
+ _channel._localCloseCode = closeCode;
+ _channel._localCloseReason = closeReason;
+ return super.close();
+ }
+}
+
+/// An enum for choosing what type [HtmlWebSocketChannel] emits for binary
+/// messages.
+class BinaryType {
+ /// Tells the channel to emit binary messages as [Blob]s.
+ static const blob = BinaryType._('blob', 'blob');
+
+ /// Tells the channel to emit binary messages as [Uint8List]s.
+ static const list = BinaryType._('list', 'arraybuffer');
+
+ /// The name of the binary type, which matches its variable name.
+ final String name;
+
+ /// The value as understood by the underlying [WebSocket] API.
+ final String value;
+
+ const BinaryType._(this.name, this.value);
+
+ @override
+ String toString() => name;
+}
diff --git a/pkgs/web_socket_channel/lib/io.dart b/pkgs/web_socket_channel/lib/io.dart
new file mode 100644
index 0000000..ff10d1a
--- /dev/null
+++ b/pkgs/web_socket_channel/lib/io.dart
@@ -0,0 +1,63 @@
+// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:io' show HttpClient, WebSocket;
+
+import 'package:web_socket/io_web_socket.dart' show IOWebSocket;
+
+import 'adapter_web_socket_channel.dart';
+import 'src/channel.dart';
+import 'src/exception.dart';
+
+/// A [WebSocketChannel] that communicates using a `dart:io` [WebSocket].
+class IOWebSocketChannel extends AdapterWebSocketChannel {
+ /// Creates a new WebSocket connection.
+ ///
+ /// Connects to [url] using [WebSocket.connect] and returns a channel that can
+ /// be used to communicate over the resulting socket. The [url] may be either
+ /// a [String] or a [Uri]. The [protocols] and [headers] parameters are the
+ /// same as [WebSocket.connect].
+ ///
+ /// [pingInterval] controls the interval for sending ping signals. If a ping
+ /// message is not answered by a pong message from the peer, the WebSocket is
+ /// assumed disconnected and the connection is closed with a `goingAway` code.
+ /// When a ping signal is sent, the pong message must be received within
+ /// [pingInterval]. It defaults to `null`, indicating that ping messages are
+ /// disabled.
+ ///
+ /// [connectTimeout] determines how long to wait for [WebSocket.connect]
+ /// before throwing a [TimeoutException]. If connectTimeout is null then the
+ /// connection process will never time-out.
+ ///
+ /// If there's an error connecting, the channel's stream emits a
+ /// [WebSocketChannelException] wrapping that error and then closes.
+ factory IOWebSocketChannel.connect(
+ Object url, {
+ Iterable<String>? protocols,
+ Map<String, dynamic>? headers,
+ Duration? pingInterval,
+ Duration? connectTimeout,
+ HttpClient? customClient,
+ }) {
+ var webSocketFuture = WebSocket.connect(
+ url.toString(),
+ headers: headers,
+ protocols: protocols,
+ customClient: customClient,
+ ).then((webSocket) => webSocket..pingInterval = pingInterval);
+
+ if (connectTimeout != null) {
+ webSocketFuture = webSocketFuture.timeout(connectTimeout);
+ }
+
+ return IOWebSocketChannel(webSocketFuture);
+ }
+
+ /// Creates a channel wrapping [webSocket].
+ IOWebSocketChannel(FutureOr<WebSocket> webSocket)
+ : super(webSocket is Future<WebSocket>
+ ? webSocket.then(IOWebSocket.fromWebSocket) as FutureOr<IOWebSocket>
+ : IOWebSocket.fromWebSocket(webSocket));
+}
diff --git a/pkgs/web_socket_channel/lib/src/channel.dart b/pkgs/web_socket_channel/lib/src/channel.dart
new file mode 100644
index 0000000..f8a560e
--- /dev/null
+++ b/pkgs/web_socket_channel/lib/src/channel.dart
@@ -0,0 +1,126 @@
+// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:convert' as convert;
+
+import 'package:async/async.dart';
+import 'package:crypto/crypto.dart';
+import 'package:stream_channel/stream_channel.dart';
+
+import '../adapter_web_socket_channel.dart';
+import 'exception.dart';
+
+const String _webSocketGUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
+
+/// A [StreamChannel] that communicates over a WebSocket.
+///
+/// This is implemented by classes that use `dart:io` and `dart:html`.
+/// The [WebSocketChannel.new] constructor can also be used on any platform to
+/// connect to use the WebSocket protocol over a pre-existing channel.
+///
+/// All implementations emit [WebSocketChannelException]s. These exceptions wrap
+/// the native exception types where possible.
+abstract interface class WebSocketChannel extends StreamChannelMixin {
+ /// The subprotocol selected by the server.
+ ///
+ /// For a client socket, this is initially `null`. After the WebSocket
+ /// connection is established the value is set to the subprotocol selected by
+ /// the server. If no subprotocol is negotiated the value will remain `null`.
+ String? get protocol;
+
+ /// The [close code][] set when the WebSocket connection is closed.
+ ///
+ /// [close code]: https://tools.ietf.org/html/rfc6455#section-7.1.5
+ ///
+ /// Before the connection has been closed, this will be `null`.
+ int? get closeCode;
+
+ /// The [close reason][] set when the WebSocket connection is closed.
+ ///
+ /// [close reason]: https://tools.ietf.org/html/rfc6455#section-7.1.6
+ ///
+ /// Before the connection has been closed, this will be `null`.
+ String? get closeReason;
+
+ /// A future that will complete when the WebSocket connection has been
+ /// established.
+ ///
+ /// This future must be complete before before data can be sent using
+ /// [WebSocketChannel.sink].
+ ///
+ /// If a connection could not be established (e.g. because of a network
+ /// issue), then this future will complete with an error.
+ ///
+ /// For example:
+ /// ```
+ /// final channel = WebSocketChannel.connect(Uri.parse('ws://example.com'));
+ ///
+ /// try {
+ /// await channel.ready;
+ /// } on SocketException catch (e) {
+ /// // Handle the exception.
+ /// } on WebSocketChannelException catch (e) {
+ /// // Handle the exception.
+ /// }
+ ///
+ /// // If `ready` completes without an error then the channel is ready to
+ /// // send data.
+ /// channel.sink.add('Hello World');
+ /// ```
+ Future<void> get ready;
+
+ /// The sink for sending values to the other endpoint.
+ ///
+ /// This supports additional arguments to [WebSocketSink.close] that provide
+ /// the remote endpoint reasons for closing the connection.
+ @override
+ WebSocketSink get sink;
+
+ /// Signs a `Sec-WebSocket-Key` header sent by a WebSocket client as part of
+ /// the [initial handshake][].
+ ///
+ /// The return value should be sent back to the client in a
+ /// `Sec-WebSocket-Accept` header.
+ ///
+ /// [initial handshake]: https://tools.ietf.org/html/rfc6455#section-4.2.2
+ static String signKey(String key)
+ // We use [codeUnits] here rather than UTF-8-decoding the string because
+ // [key] is expected to be base64 encoded, and so will be pure ASCII.
+ =>
+ convert.base64
+ .encode(sha1.convert((key + _webSocketGUID).codeUnits).bytes);
+
+ /// Creates a new WebSocket connection.
+ ///
+ /// Connects to [uri] using and returns a channel that can be used to
+ /// communicate over the resulting socket.
+ ///
+ /// The optional [protocols] parameter is the same as `WebSocket.connect`.
+ ///
+ /// A WebSocketChannel is returned synchronously, however the connection is
+ /// not established synchronously.
+ /// The [ready] future will complete after the channel is connected.
+ /// If there are errors creating the connection the [ready] future will
+ /// complete with an error.
+ static WebSocketChannel connect(Uri uri, {Iterable<String>? protocols}) =>
+ AdapterWebSocketChannel.connect(uri, protocols: protocols);
+}
+
+/// The sink exposed by a [WebSocketChannel].
+///
+/// This is like a normal [StreamSink], except that it supports extra arguments
+/// to [close].
+abstract interface class WebSocketSink implements DelegatingStreamSink {
+ /// Closes the web socket connection.
+ ///
+ /// [closeCode] and [closeReason] are the [close code][] and [reason][] sent
+ /// to the remote peer, respectively. If they are omitted, the peer will see
+ /// a "no status received" code with no reason.
+ ///
+ /// [close code]: https://tools.ietf.org/html/rfc6455#section-7.1.5
+ /// [reason]: https://tools.ietf.org/html/rfc6455#section-7.1.6
+ @override
+ Future close([int? closeCode, String? closeReason]);
+}
diff --git a/pkgs/web_socket_channel/lib/src/exception.dart b/pkgs/web_socket_channel/lib/src/exception.dart
new file mode 100644
index 0000000..11de8ab
--- /dev/null
+++ b/pkgs/web_socket_channel/lib/src/exception.dart
@@ -0,0 +1,22 @@
+// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'channel.dart';
+
+/// An exception thrown by a [WebSocketChannel].
+class WebSocketChannelException implements Exception {
+ final String? message;
+
+ /// The exception that caused this one, if available.
+ final Object? inner;
+
+ WebSocketChannelException([this.message]) : inner = null;
+
+ WebSocketChannelException.from(this.inner) : message = inner.toString();
+
+ @override
+ String toString() => message == null
+ ? 'WebSocketChannelException'
+ : 'WebSocketChannelException: $message';
+}
diff --git a/pkgs/web_socket_channel/lib/src/sink_completer.dart b/pkgs/web_socket_channel/lib/src/sink_completer.dart
new file mode 100644
index 0000000..cea8c17
--- /dev/null
+++ b/pkgs/web_socket_channel/lib/src/sink_completer.dart
@@ -0,0 +1,154 @@
+// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'channel.dart';
+
+/// A [WebSocketSink] where the destination is provided later.
+///
+/// This is like a `StreamSinkCompleter`, except that it properly forwards
+/// parameters to [WebSocketSink.close].
+class WebSocketSinkCompleter {
+ /// The sink for this completer.
+ ///
+ /// When a destination sink is provided, events that have been passed to the
+ /// sink will be forwarded to the destination.
+ ///
+ /// Events can be added to the sink either before or after a destination sink
+ /// is set.
+ final WebSocketSink sink = _CompleterSink();
+
+ /// Returns [sink] typed as a [_CompleterSink].
+ _CompleterSink get _sink => sink as _CompleterSink;
+
+ /// Sets a sink as the destination for events from the
+ /// [WebSocketSinkCompleter]'s [sink].
+ ///
+ /// The completer's [sink] will act exactly as [destinationSink].
+ ///
+ /// If the destination sink is set before events are added to [sink], further
+ /// events are forwarded directly to [destinationSink].
+ ///
+ /// If events are added to [sink] before setting the destination sink, they're
+ /// buffered until the destination is available.
+ ///
+ /// A destination sink may be set at most once.
+ void setDestinationSink(WebSocketSink destinationSink) {
+ if (_sink._destinationSink != null) {
+ throw StateError('Destination sink already set');
+ }
+ _sink._setDestinationSink(destinationSink);
+ }
+}
+
+/// [WebSocketSink] completed by [WebSocketSinkCompleter].
+class _CompleterSink implements WebSocketSink {
+ /// Controller for an intermediate sink.
+ ///
+ /// Created if the user adds events to this sink before the destination sink
+ /// is set.
+ StreamController? _controller;
+
+ /// Completer for [done].
+ ///
+ /// Created if the user requests the [done] future before the destination sink
+ /// is set.
+ Completer? _doneCompleter;
+
+ /// Destination sink for the events added to this sink.
+ ///
+ /// Set when [WebSocketSinkCompleter.setDestinationSink] is called.
+ WebSocketSink? _destinationSink;
+
+ /// The close code passed to [close].
+ int? _closeCode;
+
+ /// The close reason passed to [close].
+ String? _closeReason;
+
+ /// Whether events should be sent directly to [_destinationSink], as opposed
+ /// to going through [_controller].
+ bool get _canSendDirectly => _controller == null && _destinationSink != null;
+
+ @override
+ Future get done {
+ if (_doneCompleter != null) return _doneCompleter!.future;
+ if (_destinationSink == null) {
+ _doneCompleter = Completer.sync();
+ return _doneCompleter!.future;
+ }
+ return _destinationSink!.done;
+ }
+
+ @override
+ void add(Object? event) {
+ if (_canSendDirectly) {
+ _destinationSink!.add(event);
+ } else {
+ _ensureController().add(event);
+ }
+ }
+
+ @override
+ void addError(Object error, [StackTrace? stackTrace]) {
+ if (_canSendDirectly) {
+ _destinationSink!.addError(error, stackTrace);
+ } else {
+ _ensureController().addError(error, stackTrace);
+ }
+ }
+
+ @override
+ Future addStream(Stream stream) {
+ if (_canSendDirectly) return _destinationSink!.addStream(stream);
+
+ final controller = _ensureController();
+ return controller.addStream(stream, cancelOnError: false);
+ }
+
+ @override
+ Future close([int? closeCode, String? closeReason]) {
+ if (_canSendDirectly) {
+ _destinationSink!.close(closeCode, closeReason);
+ } else {
+ _closeCode = closeCode;
+ _closeReason = closeReason;
+ _ensureController().close();
+ }
+ return done;
+ }
+
+ /// Create [_controller] if it doesn't yet exist.
+ StreamController _ensureController() =>
+ _controller ??= StreamController(sync: true);
+
+ /// Sets the destination sink to which events from this sink will be provided.
+ ///
+ /// If set before the user adds events, events will be added directly to the
+ /// destination sink. If the user adds events earlier, an intermediate sink is
+ /// created using a stream controller, and the destination sink is linked to
+ /// it later.
+ void _setDestinationSink(WebSocketSink sink) {
+ assert(_destinationSink == null);
+ _destinationSink = sink;
+
+ // If the user has already added data, it's buffered in the controller, so
+ // we add it to the sink.
+ if (_controller != null) {
+ // Catch any error that may come from [addStream] or [sink.close]. They'll
+ // be reported through [done] anyway.
+ sink
+ .addStream(_controller!.stream)
+ .whenComplete(() => sink.close(_closeCode, _closeReason))
+ .catchError((_) {});
+ }
+
+ // If the user has already asked when the sink is done, connect the sink's
+ // done callback to that completer.
+ if (_doneCompleter != null) {
+ _doneCompleter!.complete(sink.done);
+ }
+ }
+}
diff --git a/pkgs/web_socket_channel/lib/status.dart b/pkgs/web_socket_channel/lib/status.dart
new file mode 100644
index 0000000..178e7d3
--- /dev/null
+++ b/pkgs/web_socket_channel/lib/status.dart
@@ -0,0 +1,90 @@
+// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Status codes that are defined in the WebSocket spec.
+///
+/// This library is intended to be imported with a prefix.
+///
+/// ```dart
+/// import 'package:web_socket_channel/web_socket_channel.dart';
+/// import 'package:web_socket_channel/status.dart' as status;
+///
+/// void main() async {
+/// var channel = WebSocketChannel.connect(Uri.parse('ws://localhost:1234'));
+/// // ...
+/// channel.close(status.goingAway);
+/// }
+/// ```
+library;
+
+import 'dart:core';
+
+/// The purpose for which the connection was established has been fulfilled.
+const normalClosure = 1000;
+
+/// An endpoint is "going away", such as a server going down or a browser having
+/// navigated away from a page.
+const goingAway = 1001;
+
+/// An endpoint is terminating the connection due to a protocol error.
+const protocolError = 1002;
+
+/// An endpoint is terminating the connection because it has received a type of
+/// data it cannot accept.
+///
+/// For example, an endpoint that understands only text data MAY send this if it
+/// receives a binary message).
+const unsupportedData = 1003;
+
+/// No status code was present.
+///
+/// This **must not** be set explicitly by an endpoint.
+const noStatusReceived = 1005;
+
+/// The connection was closed abnormally.
+///
+/// For example, this is used if the connection was closed without sending or
+/// receiving a Close control frame.
+///
+/// This **must not** be set explicitly by an endpoint.
+const abnormalClosure = 1006;
+
+/// An endpoint is terminating the connection because it has received data
+/// within a message that was not consistent with the type of the message.
+///
+/// For example, the endpoint may have receieved non-UTF-8 data within a text
+/// message.
+const invalidFramePayloadData = 1007;
+
+/// An endpoint is terminating the connection because it has received a message
+/// that violates its policy.
+///
+/// This is a generic status code that can be returned when there is no other
+/// more suitable status code (such as [unsupportedData] or [messageTooBig]), or
+/// if there is a need to hide specific details about the policy.
+const policyViolation = 1008;
+
+/// An endpoint is terminating the connection because it has received a message
+/// that is too big for it to process.
+const messageTooBig = 1009;
+
+/// The client is terminating the connection because it expected the server to
+/// negotiate one or more extensions, but the server didn't return them in the
+/// response message of the WebSocket handshake.
+///
+/// The list of extensions that are needed should appear in the close reason.
+/// Note that this status code is not used by the server, because it can fail
+/// the WebSocket handshake instead.
+const missingMandatoryExtension = 1010;
+
+/// The server is terminating the connection because it encountered an
+/// unexpected condition that prevented it from fulfilling the request.
+const internalServerError = 1011;
+
+/// The connection was closed due to a failure to perform a TLS handshake.
+///
+/// For example, the server certificate may not have been verified.
+///
+/// This **must not** be set explicitly by an endpoint.
+const tlsHandshakeFailed = 1015;
diff --git a/pkgs/web_socket_channel/lib/web_socket_channel.dart b/pkgs/web_socket_channel/lib/web_socket_channel.dart
new file mode 100644
index 0000000..54c05a0
--- /dev/null
+++ b/pkgs/web_socket_channel/lib/web_socket_channel.dart
@@ -0,0 +1,6 @@
+// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+export 'src/channel.dart';
+export 'src/exception.dart';
diff --git a/pkgs/web_socket_channel/pubspec.yaml b/pkgs/web_socket_channel/pubspec.yaml
new file mode 100644
index 0000000..580c9d2
--- /dev/null
+++ b/pkgs/web_socket_channel/pubspec.yaml
@@ -0,0 +1,25 @@
+name: web_socket_channel
+version: 3.0.2
+description: >-
+ StreamChannel wrappers for WebSockets. Provides a cross-platform
+ WebSocketChannel API, a cross-platform implementation of that API that
+ communicates over an underlying StreamChannel.
+repository: https://github.com/dart-lang/http/tree/master/pkgs/web_socket_channel
+
+environment:
+ sdk: ^3.3.0
+
+dependencies:
+ async: ^2.5.0
+ crypto: ^3.0.0
+ stream_channel: ^2.1.0
+ web: '>=0.5.0 <2.0.0'
+ web_socket: ^0.1.5
+
+dev_dependencies:
+ dart_flutter_team_lints: ^3.0.0
+ test: ^1.25.2
+
+topics:
+ - web
+ - http
diff --git a/pkgs/web_socket_channel/test/adapter_web_socket_channel_test.dart b/pkgs/web_socket_channel/test/adapter_web_socket_channel_test.dart
new file mode 100644
index 0000000..44ed7a9
--- /dev/null
+++ b/pkgs/web_socket_channel/test/adapter_web_socket_channel_test.dart
@@ -0,0 +1,150 @@
+// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:typed_data';
+
+import 'package:async/async.dart';
+import 'package:stream_channel/stream_channel.dart';
+import 'package:test/test.dart';
+import 'package:web_socket/web_socket.dart';
+import 'package:web_socket_channel/adapter_web_socket_channel.dart';
+import 'package:web_socket_channel/src/exception.dart';
+import 'package:web_socket_channel/web_socket_channel.dart';
+
+import 'echo_server_vm.dart'
+ if (dart.library.js_interop) 'echo_server_web.dart';
+
+void main() {
+ group('AdapterWebSocketChannel', () {
+ late Uri uri;
+ late StreamChannel<Object?> httpServerChannel;
+ late StreamQueue<Object?> httpServerQueue;
+
+ setUp(() async {
+ httpServerChannel = await startServer();
+ httpServerQueue = StreamQueue(httpServerChannel.stream);
+
+ // When run under dart2wasm, JSON numbers are always returned as [double].
+ final port = ((await httpServerQueue.next) as num).toInt();
+ uri = Uri.parse('ws://localhost:$port');
+ });
+ tearDown(() async {
+ httpServerChannel.sink.add(null);
+ });
+
+ test('failed connect', () async {
+ final channel =
+ AdapterWebSocketChannel.connect(Uri.parse('ws://notahost'));
+
+ await expectLater(
+ channel.ready, throwsA(isA<WebSocketChannelException>()));
+ });
+
+ test('good connect', () async {
+ final channel = AdapterWebSocketChannel.connect(uri);
+ await expectLater(channel.ready, completes);
+ await channel.sink.close();
+ });
+
+ test('echo empty text', () async {
+ final channel = AdapterWebSocketChannel.connect(uri);
+ await expectLater(channel.ready, completes);
+ channel.sink.add('');
+ expect(await channel.stream.first, '');
+ await channel.sink.close();
+ });
+
+ test('echo empty binary', () async {
+ final channel = AdapterWebSocketChannel.connect(uri);
+ await expectLater(channel.ready, completes);
+ channel.sink.add(Uint8List.fromList(<int>[]));
+ expect(await channel.stream.first, isEmpty);
+ await channel.sink.close();
+ });
+
+ test('echo hello', () async {
+ final channel = AdapterWebSocketChannel.connect(uri);
+ await expectLater(channel.ready, completes);
+ channel.sink.add('hello');
+ expect(await channel.stream.first, 'hello');
+ await channel.sink.close();
+ });
+
+ test('echo [1,2,3]', () async {
+ final channel = AdapterWebSocketChannel.connect(uri);
+ await expectLater(channel.ready, completes);
+ channel.sink.add([1, 2, 3]);
+ expect(await channel.stream.first, [1, 2, 3]);
+ await channel.sink.close();
+ });
+
+ test('alternative string and binary request and response', () async {
+ final channel = AdapterWebSocketChannel.connect(uri);
+ await expectLater(channel.ready, completes);
+ channel.sink.add('This count says:');
+ channel.sink.add([1, 2, 3]);
+ channel.sink.add('And then:');
+ channel.sink.add([4, 5, 6]);
+ expect(await channel.stream.take(4).toList(), [
+ 'This count says:',
+ [1, 2, 3],
+ 'And then:',
+ [4, 5, 6]
+ ]);
+ });
+
+ test('remote close', () async {
+ final channel = AdapterWebSocketChannel.connect(uri);
+ await expectLater(channel.ready, completes);
+ channel.sink.add('close'); // Asks the peer to close.
+ // Give the server time to send a close frame.
+ await Future<void>.delayed(const Duration(seconds: 1));
+ expect(channel.closeCode, 3001);
+ expect(channel.closeReason, 'you asked me to');
+ await channel.sink.close();
+ });
+
+ test('local close', () async {
+ final channel = AdapterWebSocketChannel.connect(uri);
+ await expectLater(channel.ready, completes);
+ await channel.sink.close(3005, 'please close');
+ expect(channel.closeCode, null);
+ expect(channel.closeReason, null);
+ });
+
+ test('constructor with WebSocket', () async {
+ final webSocket = await WebSocket.connect(uri);
+ final channel = AdapterWebSocketChannel(webSocket);
+
+ await expectLater(channel.ready, completes);
+ channel.sink.add('This count says:');
+ channel.sink.add([1, 2, 3]);
+ channel.sink.add('And then:');
+ channel.sink.add([4, 5, 6]);
+ expect(await channel.stream.take(4).toList(), [
+ 'This count says:',
+ [1, 2, 3],
+ 'And then:',
+ [4, 5, 6]
+ ]);
+ });
+
+ test('constructor with Future<WebSocket>', () async {
+ final webSocketFuture = WebSocket.connect(uri);
+ final channel = AdapterWebSocketChannel(webSocketFuture);
+
+ await expectLater(channel.ready, completes);
+ channel.sink.add('This count says:');
+ channel.sink.add([1, 2, 3]);
+ channel.sink.add('And then:');
+ channel.sink.add([4, 5, 6]);
+ expect(await channel.stream.take(4).toList(), [
+ 'This count says:',
+ [1, 2, 3],
+ 'And then:',
+ [4, 5, 6]
+ ]);
+ });
+ });
+}
diff --git a/pkgs/web_socket_channel/test/echo_server_vm.dart b/pkgs/web_socket_channel/test/echo_server_vm.dart
new file mode 100644
index 0000000..99ef2a2
--- /dev/null
+++ b/pkgs/web_socket_channel/test/echo_server_vm.dart
@@ -0,0 +1,34 @@
+// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:io';
+
+import 'package:stream_channel/stream_channel.dart';
+
+Future<void> hybridMain(StreamChannel<Object?> channel) async {
+ late HttpServer server;
+
+ server = (await HttpServer.bind('localhost', 0))
+ ..transform(WebSocketTransformer())
+ .listen((WebSocket webSocket) => webSocket.listen((data) {
+ if (data == 'close') {
+ webSocket.close(3001, 'you asked me to');
+ } else {
+ webSocket.add(data);
+ }
+ }));
+
+ channel.sink.add(server.port);
+ await channel
+ .stream.first; // Any writes indicates that the server should exit.
+ unawaited(server.close());
+}
+
+/// Starts an WebSocket server that echos the payload of the request.
+Future<StreamChannel<Object?>> startServer() async {
+ final controller = StreamChannelController<Object?>(sync: true);
+ unawaited(hybridMain(controller.foreign));
+ return controller.local;
+}
diff --git a/pkgs/web_socket_channel/test/echo_server_web.dart b/pkgs/web_socket_channel/test/echo_server_web.dart
new file mode 100644
index 0000000..030b702
--- /dev/null
+++ b/pkgs/web_socket_channel/test/echo_server_web.dart
@@ -0,0 +1,35 @@
+// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:stream_channel/stream_channel.dart';
+import 'package:test/test.dart';
+
+/// Starts an WebSocket server that echos the payload of the request.
+/// Copied from `echo_server_vm.dart`.
+Future<StreamChannel<Object?>> startServer() async => spawnHybridCode(r'''
+import 'dart:async';
+import 'dart:io';
+
+import 'package:stream_channel/stream_channel.dart';
+
+/// Starts an WebSocket server that echos the payload of the request.
+Future<void> hybridMain(StreamChannel<Object?> channel) async {
+ late HttpServer server;
+
+ server = (await HttpServer.bind('localhost', 0))
+ ..transform(WebSocketTransformer())
+ .listen((WebSocket webSocket) => webSocket.listen((data) {
+ if (data == 'close') {
+ webSocket.close(3001, 'you asked me to');
+ } else {
+ webSocket.add(data);
+ }
+ }));
+
+ channel.sink.add(server.port);
+ await channel
+ .stream.first; // Any writes indicates that the server should exit.
+ unawaited(server.close());
+}
+''');
diff --git a/pkgs/web_socket_channel/test/html_test.dart b/pkgs/web_socket_channel/test/html_test.dart
new file mode 100644
index 0000000..98fe43f
--- /dev/null
+++ b/pkgs/web_socket_channel/test/html_test.dart
@@ -0,0 +1,189 @@
+// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@TestOn('browser')
+library;
+
+import 'dart:async';
+import 'dart:js_interop';
+import 'dart:typed_data';
+
+import 'package:async/async.dart';
+import 'package:stream_channel/stream_channel.dart';
+import 'package:test/test.dart';
+import 'package:web/web.dart' hide BinaryType;
+import 'package:web_socket_channel/html.dart';
+import 'package:web_socket_channel/web_socket_channel.dart';
+
+extension on StreamChannel {
+ /// Handles the Wasm case where the runtime type is actually [double] instead
+ /// of the JS case where its [int].
+ Future<int> get firstAsInt async => ((await stream.first) as num).toInt();
+}
+
+void main() {
+ late int port;
+ setUpAll(() async {
+ final channel = spawnHybridCode(r'''
+ import 'dart:io';
+
+ import 'package:stream_channel/stream_channel.dart';
+
+ hybridMain(StreamChannel channel) async {
+ var server = await HttpServer.bind('localhost', 0);
+ server.transform(WebSocketTransformer()).listen((webSocket) {
+ webSocket.listen((request) {
+ webSocket.add(request);
+ });
+ });
+ channel.sink.add(server.port);
+ }
+ ''', stayAlive: true);
+
+ port = await channel.firstAsInt;
+ });
+
+ test('communicates using an existing WebSocket', () async {
+ final webSocket = WebSocket('ws://localhost:$port');
+ final channel = HtmlWebSocketChannel(webSocket);
+
+ expect(channel.ready, completes);
+
+ addTearDown(channel.sink.close);
+
+ final queue = StreamQueue(channel.stream);
+ channel.sink.add('foo');
+ expect(await queue.next, equals('foo'));
+
+ channel.sink.add(Uint8List.fromList([1, 2, 3, 4, 5]));
+ expect(
+ await _decodeBlob(await queue.next as Blob),
+ equals([1, 2, 3, 4, 5]),
+ );
+
+ webSocket.binaryType = 'arraybuffer';
+ channel.sink.add(Uint8List.fromList([1, 2, 3, 4, 5]));
+ expect(await queue.next, equals([1, 2, 3, 4, 5]));
+ });
+
+ test('communicates using an existing open WebSocket', () async {
+ final webSocket = WebSocket('ws://localhost:$port');
+ await webSocket.onOpen.first;
+
+ final channel = HtmlWebSocketChannel(webSocket);
+
+ expect(channel.ready, completes);
+
+ addTearDown(channel.sink.close);
+
+ final queue = StreamQueue(channel.stream);
+ channel.sink.add('foo');
+ expect(await queue.next, equals('foo'));
+ });
+
+ test('communicates using an connecting WebSocket', () async {
+ final webSocket = WebSocket('ws://localhost:$port');
+
+ final channel = HtmlWebSocketChannel(webSocket);
+
+ expect(channel.ready, completes);
+
+ addTearDown(channel.sink.close);
+ });
+
+ test('communicates using an existing closed WebSocket', () async {
+ final webSocket = WebSocket('ws://localhost:$port');
+ webSocket.close();
+
+ final channel = HtmlWebSocketChannel(webSocket);
+ await expectLater(
+ channel.ready,
+ throwsA(
+ isA<WebSocketChannelException>()
+ .having((p0) => p0.message, 'message', 'WebSocket state error: 2')
+ .having((p0) => p0.inner, 'inner', isNull),
+ ),
+ );
+ });
+
+ test('.connect defaults to binary lists', () async {
+ final channel = HtmlWebSocketChannel.connect('ws://localhost:$port');
+
+ expect(channel.ready, completes);
+
+ addTearDown(channel.sink.close);
+
+ final queue = StreamQueue(channel.stream);
+ channel.sink.add('foo');
+ expect(await queue.next, equals('foo'));
+
+ channel.sink.add(Uint8List.fromList([1, 2, 3, 4, 5]));
+ expect(await queue.next, equals([1, 2, 3, 4, 5]));
+ });
+
+ test('.connect defaults to binary lists using platform independent api',
+ () async {
+ final channel = WebSocketChannel.connect(Uri.parse('ws://localhost:$port'));
+
+ expect(channel.ready, completes);
+
+ addTearDown(channel.sink.close);
+
+ final queue = StreamQueue(channel.stream);
+ channel.sink.add('foo');
+ expect(await queue.next, equals('foo'));
+
+ channel.sink.add(Uint8List.fromList([1, 2, 3, 4, 5]));
+ expect(await queue.next, equals([1, 2, 3, 4, 5]));
+ });
+
+ test('.connect can use blobs', () async {
+ final channel = HtmlWebSocketChannel.connect('ws://localhost:$port',
+ binaryType: BinaryType.blob);
+
+ expect(channel.ready, completes);
+
+ addTearDown(channel.sink.close);
+
+ final queue = StreamQueue(channel.stream);
+ channel.sink.add('foo');
+ expect(await queue.next, equals('foo'));
+
+ channel.sink.add(Uint8List.fromList([1, 2, 3, 4, 5]));
+ expect(
+ await _decodeBlob(await queue.next as Blob), equals([1, 2, 3, 4, 5]));
+ });
+
+ test('.connect wraps a connection error in WebSocketChannelException',
+ () async {
+ // Spawn a server that will immediately reject the connection.
+ final serverChannel = spawnHybridCode(r'''
+ import 'dart:io';
+
+ import 'package:stream_channel/stream_channel.dart';
+
+ hybridMain(StreamChannel channel) async {
+ var server = await ServerSocket.bind('localhost', 0);
+ server.listen((socket) {
+ socket.close();
+ });
+ channel.sink.add(server.port);
+ }
+ ''');
+
+ // TODO(nweiz): Make this channel use a port number that's guaranteed to be
+ // invalid.
+ final channel = HtmlWebSocketChannel.connect(
+ 'ws://localhost:${await serverChannel.firstAsInt}');
+ expect(channel.ready, throwsA(isA<WebSocketChannelException>()));
+ expect(channel.stream.toList(), throwsA(isA<WebSocketChannelException>()));
+ });
+}
+
+Future<List<int>> _decodeBlob(Blob blob) async {
+ final reader = FileReader();
+ reader.readAsArrayBuffer(blob);
+ await reader.onLoadEnd.first;
+ return (reader.result as JSArrayBuffer).toDart.asUint8List();
+}
diff --git a/pkgs/web_socket_channel/test/io_test.dart b/pkgs/web_socket_channel/test/io_test.dart
new file mode 100644
index 0000000..2d5d578
--- /dev/null
+++ b/pkgs/web_socket_channel/test/io_test.dart
@@ -0,0 +1,249 @@
+// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@TestOn('vm')
+library;
+
+import 'dart:async';
+import 'dart:io';
+
+import 'package:test/test.dart';
+import 'package:web_socket_channel/io.dart';
+import 'package:web_socket_channel/web_socket_channel.dart';
+
+void main() {
+ HttpServer server;
+
+ test('communicates using existing WebSockets', () async {
+ server = await HttpServer.bind('localhost', 0);
+ addTearDown(server.close);
+ server.transform(WebSocketTransformer()).listen((WebSocket webSocket) {
+ final channel = IOWebSocketChannel(webSocket);
+ channel.sink.add('hello!');
+ channel.stream.listen((request) {
+ expect(request, equals('ping'));
+ channel.sink.add('pong');
+ channel.sink.close(3678, 'raisin');
+ });
+ });
+
+ final webSocket = await WebSocket.connect('ws://localhost:${server.port}');
+ final channel = IOWebSocketChannel(webSocket);
+
+ expect(channel.ready, completes);
+
+ var n = 0;
+ channel.stream.listen((message) {
+ if (n == 0) {
+ expect(message, equals('hello!'));
+ channel.sink.add('ping');
+ } else if (n == 1) {
+ expect(message, equals('pong'));
+ } else {
+ fail('Only expected two messages.');
+ }
+ n++;
+ }, onDone: expectAsync0(() {
+ expect(channel.closeCode, equals(3678));
+ expect(channel.closeReason, equals('raisin'));
+ }));
+ });
+
+ test('.connect communicates immediately', () async {
+ server = await HttpServer.bind('localhost', 0);
+ addTearDown(server.close);
+ server.transform(WebSocketTransformer()).listen((WebSocket webSocket) {
+ final channel = IOWebSocketChannel(webSocket);
+ channel.stream.listen((request) {
+ expect(request, equals('ping'));
+ channel.sink.add('pong');
+ });
+ });
+
+ final channel = IOWebSocketChannel.connect('ws://localhost:${server.port}');
+
+ expect(channel.ready, completes);
+
+ channel.sink.add('ping');
+
+ channel.stream.listen(expectAsync1((message) {
+ expect(message, equals('pong'));
+ channel.sink.close(3678, 'raisin');
+ }), onDone: expectAsync0(() {}));
+ });
+
+ test('.connect communicates immediately using platform independent api',
+ () async {
+ server = await HttpServer.bind('localhost', 0);
+ addTearDown(server.close);
+ server.transform(WebSocketTransformer()).listen((WebSocket webSocket) {
+ final channel = IOWebSocketChannel(webSocket);
+ channel.stream.listen((request) {
+ expect(request, equals('ping'));
+ channel.sink.add('pong');
+ });
+ });
+
+ final channel =
+ WebSocketChannel.connect(Uri.parse('ws://localhost:${server.port}'));
+
+ expect(channel.ready, completes);
+
+ channel.sink.add('ping');
+
+ channel.stream.listen(expectAsync1((message) {
+ expect(message, equals('pong'));
+ channel.sink.close(3678, 'raisin');
+ }), onDone: expectAsync0(() {}));
+ });
+
+ test('.connect with an immediate call to close', () async {
+ server = await HttpServer.bind('localhost', 0);
+ addTearDown(server.close);
+ server.transform(WebSocketTransformer()).listen((WebSocket webSocket) {
+ expect(() async {
+ final channel = IOWebSocketChannel(webSocket);
+ await channel.stream.drain<void>();
+ expect(channel.closeCode, equals(3678));
+ expect(channel.closeReason, equals('raisin'));
+ }(), completes);
+ });
+
+ final channel = IOWebSocketChannel.connect('ws://localhost:${server.port}');
+
+ expect(channel.ready, completes);
+
+ await channel.sink.close(3678, 'raisin');
+ });
+
+ test('.connect wraps a connection error in WebSocketChannelException',
+ () async {
+ server = await HttpServer.bind('localhost', 0);
+ addTearDown(server.close);
+ server.listen((request) {
+ request.response.statusCode = 404;
+ request.response.close();
+ });
+
+ final channel = IOWebSocketChannel.connect('ws://localhost:${server.port}');
+ expect(channel.ready, throwsA(isA<WebSocketChannelException>()));
+ expect(channel.stream.drain<void>(),
+ throwsA(isA<WebSocketChannelException>()));
+ });
+
+ test('.protocols fail', () async {
+ const passedProtocol = 'passed-protocol';
+ const failedProtocol = 'failed-protocol';
+ String selector(List<String> receivedProtocols) => passedProtocol;
+
+ server = await HttpServer.bind('localhost', 0);
+ addTearDown(server.close);
+ server.listen((HttpRequest request) {
+ expect(
+ WebSocketTransformer.upgrade(request, protocolSelector: selector),
+ throwsException,
+ );
+ });
+
+ final channel = IOWebSocketChannel.connect(
+ 'ws://localhost:${server.port}',
+ protocols: [failedProtocol],
+ );
+ expect(channel.ready, throwsA(isA<WebSocketChannelException>()));
+ expect(
+ channel.stream.drain<void>(),
+ throwsA(isA<WebSocketChannelException>()),
+ );
+ });
+
+ test('.protocols pass', () async {
+ const passedProtocol = 'passed-protocol';
+ String selector(List<String> receivedProtocols) => passedProtocol;
+
+ server = await HttpServer.bind('localhost', 0);
+ addTearDown(server.close);
+ server.listen((HttpRequest request) async {
+ final webSocket = await WebSocketTransformer.upgrade(
+ request,
+ protocolSelector: selector,
+ );
+ expect(webSocket.protocol, passedProtocol);
+ await webSocket.close();
+ });
+
+ final channel = IOWebSocketChannel.connect('ws://localhost:${server.port}',
+ protocols: [passedProtocol]);
+
+ expect(channel.ready, completes);
+
+ await channel.stream.drain<void>();
+ expect(channel.protocol, passedProtocol);
+ });
+
+ test('.connects with a timeout parameters specified', () async {
+ server = await HttpServer.bind('localhost', 0);
+ addTearDown(server.close);
+ server.transform(WebSocketTransformer()).listen((webSocket) {
+ expect(() async {
+ final channel = IOWebSocketChannel(webSocket);
+ await channel.stream.drain<void>();
+ expect(channel.closeCode, equals(3678));
+ expect(channel.closeReason, equals('raisin'));
+ }(), completes);
+ });
+
+ final channel = IOWebSocketChannel.connect(
+ 'ws://localhost:${server.port}',
+ connectTimeout: const Duration(milliseconds: 1000),
+ );
+ expect(channel.ready, completes);
+ await channel.sink.close(3678, 'raisin');
+ });
+
+ test('.respects timeout parameter when trying to connect', () async {
+ server = await HttpServer.bind('localhost', 0);
+ addTearDown(server.close);
+ server
+ .transform(StreamTransformer<HttpRequest, HttpRequest>.fromHandlers(
+ handleData: (data, sink) {
+ // Wait before we handle this request, to give the timeout a chance to
+ // kick in. We still want to make sure that we handle the request
+ // afterwards to not have false positives with the timeout
+ Timer(const Duration(milliseconds: 800), () {
+ sink.add(data);
+ });
+ }))
+ .transform(WebSocketTransformer())
+ .listen((webSocket) {
+ final channel = IOWebSocketChannel(webSocket);
+ channel.stream.drain<void>();
+ });
+
+ final channel = IOWebSocketChannel.connect(
+ 'ws://localhost:${server.port}',
+ connectTimeout: const Duration(milliseconds: 500),
+ );
+
+ expect(channel.ready, throwsA(isA<TimeoutException>()));
+ expect(channel.stream.drain<void>(), throwsA(anything));
+ });
+
+ test('.custom client is passed through', () async {
+ server = await HttpServer.bind('localhost', 0);
+ addTearDown(server.close);
+ server.transform(StreamTransformer<HttpRequest, HttpRequest>.fromHandlers(
+ handleData: (data, sink) {
+ expect(data.headers['user-agent'], ['custom']);
+ sink.add(data);
+ },
+ )).transform(WebSocketTransformer());
+
+ final channel = IOWebSocketChannel.connect(
+ 'ws://localhost:${server.port}',
+ customClient: HttpClient()..userAgent = 'custom',
+ );
+
+ expect(channel.ready, completes);
+ });
+}