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. | [![pub package](https://img.shields.io/pub/v/http_profile.svg)](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. | [![pub package](https://img.shields.io/pub/v/ok_http.svg)](https://pub.dev/packages/ok_http) |
 | [web_socket](pkgs/web_socket/) | Any easy-to-use library for communicating with WebSockets that has multiple implementations. | [![pub package](https://img.shields.io/pub/v/web_socket.svg)](https://pub.dev/packages/web_socket) |
+| [web_socket_channel](pkgs/web_socket_channel/) | StreamChannel wrappers for WebSockets. Provides a cross-platform WebSocketChannel API. | [![package issues](https://img.shields.io/badge/package:web_socket_channel-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Aweb_socket_channel) | [![pub package](https://img.shields.io/pub/v/web_socket_channel.svg)](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 @@
+[![pub package](https://img.shields.io/pub/v/web_socket_channel.svg)](https://pub.dev/packages/web_socket_channel)
+[![package publisher](https://img.shields.io/pub/publisher/web_socket_channel.svg)](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);
+  });
+}