From 75e01f4736118046f740ba65c3d633262767075e Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Tue, 20 Feb 2024 12:56:50 -0800 Subject: [PATCH] Create a simple WebSocket interface (#1128) --- .github/workflows/dart.yml | 104 ++++++++++--- pkgs/web_socket/CHANGELOG.md | 3 + pkgs/web_socket/README.md | 2 + .../example/web_socket_example.dart | 3 + pkgs/web_socket/lib/src/web_socket.dart | 141 ++++++++++++++++++ pkgs/web_socket/lib/web_socket.dart | 4 + pkgs/web_socket/mono_pkg.yaml | 10 ++ pkgs/web_socket/pubspec.yaml | 11 ++ 8 files changed, 256 insertions(+), 22 deletions(-) create mode 100644 pkgs/web_socket/CHANGELOG.md create mode 100644 pkgs/web_socket/README.md create mode 100644 pkgs/web_socket/example/web_socket_example.dart create mode 100644 pkgs/web_socket/lib/src/web_socket.dart create mode 100644 pkgs/web_socket/lib/web_socket.dart create mode 100644 pkgs/web_socket/mono_pkg.yaml create mode 100644 pkgs/web_socket/pubspec.yaml diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index 3de19a3ec6..22c1e5205c 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -100,6 +100,36 @@ jobs: if: "always() && steps.pkgs_http_client_conformance_tests_pub_upgrade.conclusion == 'success'" working-directory: pkgs/http_client_conformance_tests job_004: + name: "analyze_and_format; linux; Dart 3.2.6; PKG: pkgs/web_socket; `dart analyze --fatal-infos`" + runs-on: ubuntu-latest + steps: + - name: Cache Pub hosted dependencies + uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 + with: + path: "~/.pub-cache/hosted" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.2.6;packages:pkgs/web_socket;commands:analyze_1" + restore-keys: | + os:ubuntu-latest;pub-cache-hosted;sdk:3.2.6;packages:pkgs/web_socket + os:ubuntu-latest;pub-cache-hosted;sdk:3.2.6 + os:ubuntu-latest;pub-cache-hosted + os:ubuntu-latest + - name: Setup Dart SDK + uses: dart-lang/setup-dart@fedb1266e91cf51be2fdb382869461a434b920a3 + with: + sdk: "3.2.6" + - id: checkout + name: Checkout repository + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + - id: pkgs_web_socket_pub_upgrade + name: pkgs/web_socket; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: pkgs/web_socket + - name: "pkgs/web_socket; dart analyze --fatal-infos" + run: dart analyze --fatal-infos + if: "always() && steps.pkgs_web_socket_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/web_socket + job_005: name: "analyze_and_format; linux; Dart 3.3.0; PKG: pkgs/http; `dart analyze --fatal-infos`" runs-on: ubuntu-latest steps: @@ -129,17 +159,17 @@ jobs: run: dart analyze --fatal-infos if: "always() && steps.pkgs_http_pub_upgrade.conclusion == 'success'" working-directory: pkgs/http - job_005: - name: "analyze_and_format; linux; Dart dev; PKGS: pkgs/http, pkgs/http_client_conformance_tests, pkgs/http_profile; `dart analyze --fatal-infos`" + job_006: + name: "analyze_and_format; linux; Dart dev; PKGS: pkgs/http, pkgs/http_client_conformance_tests, pkgs/http_profile, pkgs/web_socket; `dart analyze --fatal-infos`" runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 with: path: "~/.pub-cache/hosted" - key: "os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/http-pkgs/http_client_conformance_tests-pkgs/http_profile;commands:analyze_1" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/http-pkgs/http_client_conformance_tests-pkgs/http_profile-pkgs/web_socket;commands:analyze_1" restore-keys: | - os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/http-pkgs/http_client_conformance_tests-pkgs/http_profile + os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/http-pkgs/http_client_conformance_tests-pkgs/http_profile-pkgs/web_socket os:ubuntu-latest;pub-cache-hosted;sdk:dev os:ubuntu-latest;pub-cache-hosted os:ubuntu-latest @@ -177,17 +207,26 @@ jobs: run: dart analyze --fatal-infos if: "always() && steps.pkgs_http_profile_pub_upgrade.conclusion == 'success'" working-directory: pkgs/http_profile - job_006: - name: "analyze_and_format; linux; Dart dev; PKGS: pkgs/http, pkgs/http_client_conformance_tests, pkgs/http_profile; `dart format --output=none --set-exit-if-changed .`" + - id: pkgs_web_socket_pub_upgrade + name: pkgs/web_socket; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: pkgs/web_socket + - name: "pkgs/web_socket; dart analyze --fatal-infos" + run: dart analyze --fatal-infos + if: "always() && steps.pkgs_web_socket_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/web_socket + job_007: + name: "analyze_and_format; linux; Dart dev; PKGS: pkgs/http, pkgs/http_client_conformance_tests, pkgs/http_profile, pkgs/web_socket; `dart format --output=none --set-exit-if-changed .`" runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 with: path: "~/.pub-cache/hosted" - key: "os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/http-pkgs/http_client_conformance_tests-pkgs/http_profile;commands:format" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/http-pkgs/http_client_conformance_tests-pkgs/http_profile-pkgs/web_socket;commands:format" restore-keys: | - os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/http-pkgs/http_client_conformance_tests-pkgs/http_profile + os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/http-pkgs/http_client_conformance_tests-pkgs/http_profile-pkgs/web_socket os:ubuntu-latest;pub-cache-hosted;sdk:dev os:ubuntu-latest;pub-cache-hosted os:ubuntu-latest @@ -225,7 +264,16 @@ jobs: run: "dart format --output=none --set-exit-if-changed ." if: "always() && steps.pkgs_http_profile_pub_upgrade.conclusion == 'success'" working-directory: pkgs/http_profile - job_007: + - id: pkgs_web_socket_pub_upgrade + name: pkgs/web_socket; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: pkgs/web_socket + - name: "pkgs/web_socket; dart format --output=none --set-exit-if-changed ." + run: "dart format --output=none --set-exit-if-changed ." + if: "always() && steps.pkgs_web_socket_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/web_socket + job_008: name: "analyze_and_format; linux; Flutter stable; PKG: pkgs/flutter_http_example; `dart format --output=none --set-exit-if-changed .`" runs-on: ubuntu-latest steps: @@ -255,7 +303,7 @@ jobs: run: "dart format --output=none --set-exit-if-changed ." if: "always() && steps.pkgs_flutter_http_example_pub_upgrade.conclusion == 'success'" working-directory: pkgs/flutter_http_example - job_008: + job_009: name: "analyze_and_format; linux; Flutter stable; PKG: pkgs/flutter_http_example; `flutter analyze --fatal-infos`" runs-on: ubuntu-latest steps: @@ -285,7 +333,7 @@ jobs: run: flutter analyze --fatal-infos if: "always() && steps.pkgs_flutter_http_example_pub_upgrade.conclusion == 'success'" working-directory: pkgs/flutter_http_example - job_009: + job_010: name: "unit_test; linux; Dart 3.0.0; PKG: pkgs/http_profile; `dart test --platform vm`" runs-on: ubuntu-latest steps: @@ -324,7 +372,8 @@ jobs: - job_006 - job_007 - job_008 - job_010: + - job_009 + job_011: name: "unit_test; linux; Dart 3.3.0; PKG: pkgs/http; `dart run --define=no_default_http_client=true test/no_default_http_client_test.dart`" runs-on: ubuntu-latest steps: @@ -363,7 +412,8 @@ jobs: - job_006 - job_007 - job_008 - job_011: + - job_009 + job_012: name: "unit_test; linux; Dart 3.3.0; PKG: pkgs/http; `dart test --platform chrome`" runs-on: ubuntu-latest steps: @@ -402,7 +452,8 @@ jobs: - job_006 - job_007 - job_008 - job_012: + - job_009 + job_013: name: "unit_test; linux; Dart 3.3.0; PKG: pkgs/http; `dart test --platform vm`" runs-on: ubuntu-latest steps: @@ -441,7 +492,8 @@ jobs: - job_006 - job_007 - job_008 - job_013: + - job_009 + job_014: name: "unit_test; linux; Dart dev; PKG: pkgs/http; `dart run --define=no_default_http_client=true test/no_default_http_client_test.dart`" runs-on: ubuntu-latest steps: @@ -480,7 +532,8 @@ jobs: - job_006 - job_007 - job_008 - job_014: + - job_009 + job_015: name: "unit_test; linux; Dart dev; PKG: pkgs/http; `dart test --platform chrome`" runs-on: ubuntu-latest steps: @@ -519,7 +572,8 @@ jobs: - job_006 - job_007 - job_008 - job_015: + - job_009 + job_016: name: "unit_test; linux; Dart dev; PKGS: pkgs/http, pkgs/http_profile; `dart test --platform vm`" runs-on: ubuntu-latest steps: @@ -567,7 +621,8 @@ jobs: - job_006 - job_007 - job_008 - job_016: + - job_009 + job_017: name: "unit_test; linux; Dart dev; PKG: pkgs/http; `dart test --test-randomize-ordering-seed=random -p chrome -c dart2wasm`" runs-on: ubuntu-latest steps: @@ -606,7 +661,8 @@ jobs: - job_006 - job_007 - job_008 - job_017: + - job_009 + job_018: name: "unit_test; linux; Flutter stable; PKG: pkgs/flutter_http_example; `flutter test --platform chrome`" runs-on: ubuntu-latest steps: @@ -645,7 +701,8 @@ jobs: - job_006 - job_007 - job_008 - job_018: + - job_009 + job_019: name: "unit_test; linux; Flutter stable; PKG: pkgs/flutter_http_example; `flutter test`" runs-on: ubuntu-latest steps: @@ -684,7 +741,8 @@ jobs: - job_006 - job_007 - job_008 - job_019: + - job_009 + job_020: name: "unit_test; macos; Flutter stable; PKG: pkgs/flutter_http_example; `flutter test`" runs-on: macos-latest steps: @@ -723,7 +781,8 @@ jobs: - job_006 - job_007 - job_008 - job_020: + - job_009 + job_021: name: "unit_test; windows; Flutter stable; PKG: pkgs/flutter_http_example; `flutter test`" runs-on: windows-latest steps: @@ -752,3 +811,4 @@ jobs: - job_006 - job_007 - job_008 + - job_009 diff --git a/pkgs/web_socket/CHANGELOG.md b/pkgs/web_socket/CHANGELOG.md new file mode 100644 index 0000000000..3d138c0f6e --- /dev/null +++ b/pkgs/web_socket/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.1.0-wip + +- Abstract interface definition. diff --git a/pkgs/web_socket/README.md b/pkgs/web_socket/README.md new file mode 100644 index 0000000000..e43843ebc9 --- /dev/null +++ b/pkgs/web_socket/README.md @@ -0,0 +1,2 @@ +TODO: Put a short description of the package here that helps potential users +know whether this package might be useful for them. diff --git a/pkgs/web_socket/example/web_socket_example.dart b/pkgs/web_socket/example/web_socket_example.dart new file mode 100644 index 0000000000..6cb625c543 --- /dev/null +++ b/pkgs/web_socket/example/web_socket_example.dart @@ -0,0 +1,3 @@ +void main() { + // TODO: add an example. +} diff --git a/pkgs/web_socket/lib/src/web_socket.dart b/pkgs/web_socket/lib/src/web_socket.dart new file mode 100644 index 0000000000..ffc0a3844c --- /dev/null +++ b/pkgs/web_socket/lib/src/web_socket.dart @@ -0,0 +1,141 @@ +import 'dart:typed_data'; + +/// An event received from the peer through the [WebSocket]. +sealed class WebSocketEvent {} + +/// Text data received from the peer through the [WebSocket]. +/// +/// See [WebSocket.events]. +final class TextDataReceived extends WebSocketEvent { + final String text; + TextDataReceived(this.text); + + @override + bool operator ==(Object other) => + other is TextDataReceived && other.text == text; + + @override + int get hashCode => text.hashCode; +} + +/// Binary data received from the peer through the [WebSocket]. +/// +/// See [WebSocket.events]. +final class BinaryDataReceived extends WebSocketEvent { + final Uint8List data; + BinaryDataReceived(this.data); + + @override + bool operator ==(Object other) { + if (other is BinaryDataReceived && other.data.length == data.length) { + for (var i = 0; i < data.length; ++i) { + if (other.data[i] != data[i]) return false; + } + return true; + } + return false; + } + + @override + int get hashCode => data.hashCode; + + @override + String toString() => 'BinaryDataReceived($data)'; +} + +/// A close notification (Close frame) received from the peer through the +/// [WebSocket] or a failure indication. +/// +/// See [WebSocket.events]. +final class CloseReceived extends WebSocketEvent { + /// A numerical code indicating the reason why the WebSocket was closed. + /// + /// See [RFC-6455 7.4](https://www.rfc-editor.org/rfc/rfc6455.html#section-7.4) + /// for guidance on how to interpret these codes. + final int? code; + + /// A textual explanation of the reason why the WebSocket was closed. + /// + /// Will be empty if the peer did not specify a reason. + final String reason; + + CloseReceived([this.code, this.reason = '']); + + @override + bool operator ==(Object other) => + other is CloseReceived && other.code == code && other.reason == reason; + + @override + int get hashCode => [code, reason].hashCode; + + @override + String toString() => 'CloseReceived($code, $reason)'; +} + +class WebSocketException implements Exception { + final String message; + WebSocketException([this.message = '']); +} + +/// Thrown if [WebSocket.sendText], [WebSocket.sendBytes], or +/// [WebSocket.close] is called when the [WebSocket] is closed. +class WebSocketConnectionClosed extends WebSocketException { + WebSocketConnectionClosed([super.message = 'Connection Closed']); +} + +/// The interface for WebSocket connections. +/// +/// TODO: insert a usage example. +abstract interface class WebSocket { + /// Sends text data to the connected peer. + /// + /// Throws [WebSocketConnectionClosed] if the [WebSocket] is + /// closed (either through [close] or by the peer). + /// + /// Data sent through [sendText] will be silently discarded if the peer is + /// disconnected but the disconnect has not yet been detected. + void sendText(String s); + + /// Sends binary data to the connected peer. + /// + /// Throws [WebSocketConnectionClosed] if the [WebSocket] is + /// closed (either through [close] or by the peer). + /// + /// Data sent through [sendBytes] will be silently discarded if the peer is + /// disconnected but the disconnect has not yet been detected. + void sendBytes(Uint8List b); + + /// Closes the WebSocket connection and the [events] `Stream`. + /// + /// Sends a Close frame to the peer. If the optional [code] and [reason] + /// arguments are given, they will be included in the Close frame. If no + /// [code] is set then the peer will see a 1005 status code. If no [reason] + /// is set then the peer will not receive a reason string. + /// + /// Throws a [RangeError] if [code] is not in the range 3000-4999. + /// + /// Throws an [ArgumentError] if [reason] is longer than 123 bytes when + /// encoded as UTF-8 + /// + /// Throws [WebSocketConnectionClosed] if the connection is already + /// closed (including by the peer). + Future close([int? code, String? reason]); + + /// A [Stream] of [WebSocketEvent] received from the peer. + /// + /// Data received by the peer will be delivered as a [TextDataReceived] or + /// [BinaryDataReceived]. + /// + /// If a [CloseReceived] event is received then the [Stream] will be closed. A + /// [CloseReceived] event indicates either that: + /// + /// - A close frame was received from the peer. [CloseReceived.code] and + /// [CloseReceived.reason] will be set by the peer. + /// - A failure occured (e.g. the peer disconnected). [CloseReceived.code] and + /// [CloseReceived.reason] will be a failure code defined by + /// (RFC-6455)[https://www.rfc-editor.org/rfc/rfc6455.html#section-7.4.1] + /// (e.g. 1006). + /// + /// Errors will never appear in this [Stream]. + Stream get events; +} diff --git a/pkgs/web_socket/lib/web_socket.dart b/pkgs/web_socket/lib/web_socket.dart new file mode 100644 index 0000000000..b901ebc76a --- /dev/null +++ b/pkgs/web_socket/lib/web_socket.dart @@ -0,0 +1,4 @@ +/// TODO: write this doc string. +library; + +export 'src/web_socket.dart'; diff --git a/pkgs/web_socket/mono_pkg.yaml b/pkgs/web_socket/mono_pkg.yaml new file mode 100644 index 0000000000..16e4e7a5f3 --- /dev/null +++ b/pkgs/web_socket/mono_pkg.yaml @@ -0,0 +1,10 @@ +sdk: +- pubspec +- dev + +stages: +- analyze_and_format: + - analyze: --fatal-infos + - format: + sdk: + - dev diff --git a/pkgs/web_socket/pubspec.yaml b/pkgs/web_socket/pubspec.yaml new file mode 100644 index 0000000000..237da791ea --- /dev/null +++ b/pkgs/web_socket/pubspec.yaml @@ -0,0 +1,11 @@ +name: web_socket +description: "TODO: enter a descirption here" +publish_to: none +repository: https://github.com/dart-lang/http/tree/master/pkgs/web_socket + +environment: + sdk: ^3.2.6 + +dev_dependencies: + dart_flutter_team_lints: ^2.0.0 + test: ^1.24.0