From 084456f9a652a096aa8c0a1d9fcf134d88a95b3a Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Thu, 7 Mar 2024 18:14:09 -0800 Subject: [PATCH 01/11] Add a WebSocket implementation to package:cupertino_http --- .../example/integration_test/utils_test.dart | 31 ++- .../web_socket_conformance_test.dart | 10 + pkgs/cupertino_http/example/pubspec.yaml | 2 + pkgs/cupertino_http/lib/cupertino_http.dart | 1 + .../cupertino_http/lib/src/cupertino_api.dart | 37 +++- .../lib/src/cupertino_web_socket.dart | 178 ++++++++++++++++++ pkgs/cupertino_http/lib/src/utils.dart | 13 +- pkgs/cupertino_http/pubspec.yaml | 2 + 8 files changed, 266 insertions(+), 8 deletions(-) create mode 100644 pkgs/cupertino_http/example/integration_test/web_socket_conformance_test.dart create mode 100644 pkgs/cupertino_http/lib/src/cupertino_web_socket.dart diff --git a/pkgs/cupertino_http/example/integration_test/utils_test.dart b/pkgs/cupertino_http/example/integration_test/utils_test.dart index 1bf1ca6f40..315282a6be 100644 --- a/pkgs/cupertino_http/example/integration_test/utils_test.dart +++ b/pkgs/cupertino_http/example/integration_test/utils_test.dart @@ -20,11 +20,11 @@ void main() { }); }); - group('stringDictToMap', () { + group('stringNSDictionaryToMap', () { test('empty input', () { final d = ncb.NSMutableDictionary.new1(linkedLibs); - expect(stringDictToMap(d), {}); + expect(stringNSDictionaryToMap(d), {}); }); test('single string input', () { @@ -32,7 +32,7 @@ void main() { ..setObject_forKey_( 'value'.toNSString(linkedLibs), 'key'.toNSString(linkedLibs)); - expect(stringDictToMap(d), {'key': 'value'}); + expect(stringNSDictionaryToMap(d), {'key': 'value'}); }); test('multiple string input', () { @@ -43,8 +43,31 @@ void main() { 'value2'.toNSString(linkedLibs), 'key2'.toNSString(linkedLibs)) ..setObject_forKey_( 'value3'.toNSString(linkedLibs), 'key3'.toNSString(linkedLibs)); - expect(stringDictToMap(d), + expect(stringNSDictionaryToMap(d), {'key1': 'value1', 'key2': 'value2', 'key3': 'value3'}); }); }); + + group('stringIterableToNSArray', () { + test('empty input', () { + final array = stringIterableToNSArray([]); + expect(array.count, 0); + }); + + test('single string input', () { + final array = stringIterableToNSArray(['apple']); + expect(array.count, 1); + expect( + ncb.NSString.castFrom(array.objectAtIndex_(0)).toString(), 'apple'); + }); + + test('multiple string input', () { + final array = stringIterableToNSArray(['apple', 'banana']); + expect(array.count, 2); + expect( + ncb.NSString.castFrom(array.objectAtIndex_(0)).toString(), 'apple'); + expect( + ncb.NSString.castFrom(array.objectAtIndex_(1)).toString(), 'banana'); + }); + }); } diff --git a/pkgs/cupertino_http/example/integration_test/web_socket_conformance_test.dart b/pkgs/cupertino_http/example/integration_test/web_socket_conformance_test.dart new file mode 100644 index 0000000000..68e5f80322 --- /dev/null +++ b/pkgs/cupertino_http/example/integration_test/web_socket_conformance_test.dart @@ -0,0 +1,10 @@ +// 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:cupertino_http/cupertino_http.dart'; +import 'package:web_socket_conformance_tests/web_socket_conformance_tests.dart'; + +void main() { + testAll(CupertinoWebSocket.connect); +} diff --git a/pkgs/cupertino_http/example/pubspec.yaml b/pkgs/cupertino_http/example/pubspec.yaml index 8d61e62bed..f394051994 100644 --- a/pkgs/cupertino_http/example/pubspec.yaml +++ b/pkgs/cupertino_http/example/pubspec.yaml @@ -31,6 +31,8 @@ dev_dependencies: integration_test: sdk: flutter test: ^1.21.1 + web_socket_conformance_tests: + path: ../../web_socket_conformance_tests/ flutter: uses-material-design: true diff --git a/pkgs/cupertino_http/lib/cupertino_http.dart b/pkgs/cupertino_http/lib/cupertino_http.dart index 243ac81436..68691b8ab4 100644 --- a/pkgs/cupertino_http/lib/cupertino_http.dart +++ b/pkgs/cupertino_http/lib/cupertino_http.dart @@ -88,3 +88,4 @@ import 'src/cupertino_client.dart'; export 'src/cupertino_api.dart'; export 'src/cupertino_client.dart'; +export 'src/cupertino_web_socket.dart'; diff --git a/pkgs/cupertino_http/lib/src/cupertino_api.dart b/pkgs/cupertino_http/lib/src/cupertino_api.dart index 5cebd7ed42..780ddde0ec 100644 --- a/pkgs/cupertino_http/lib/src/cupertino_api.dart +++ b/pkgs/cupertino_http/lib/src/cupertino_api.dart @@ -352,7 +352,7 @@ class URLSessionConfiguration Map? get httpAdditionalHeaders { if (_nsObject.HTTPAdditionalHeaders case var additionalHeaders?) { final headers = ncb.NSDictionary.castFrom(additionalHeaders); - return stringDictToMap(headers); + return stringNSDictionaryToMap(headers); } return null; } @@ -628,7 +628,7 @@ class HTTPURLResponse extends URLResponse { Map get allHeaderFields { final headers = ncb.NSDictionary.castFrom(_httpUrlResponse.allHeaderFields!); - return stringDictToMap(headers); + return stringNSDictionaryToMap(headers); } @override @@ -992,7 +992,7 @@ class URLRequest extends _ObjectHolder { return null; } else { final headers = ncb.NSDictionary.castFrom(_nsObject.allHTTPHeaderFields!); - return stringDictToMap(headers); + return stringNSDictionaryToMap(headers); } } @@ -1584,4 +1584,35 @@ class URLSession extends _ObjectHolder { onWebSocketTaskClosed: _onWebSocketTaskClosed); return task; } + + /// Creates a [URLSessionWebSocketTask] that represents a connection to a + /// WebSocket endpoint. + /// + /// See [NSURLSession webSocketTaskWithURL:protocols:](https://developer.apple.com/documentation/foundation/nsurlsession/3181172-websockettaskwithurl) + URLSessionWebSocketTask webSocketTaskWithURL(Uri uri, + {Iterable? protocols}) { + if (_isBackground) { + throw UnsupportedError( + 'WebSocket tasks are not supported in background sessions'); + } + + final URLSessionWebSocketTask task; + if (protocols == null) { + task = URLSessionWebSocketTask._( + _nsObject.webSocketTaskWithURL_(uriToNSURL(uri))); + } else { + task = URLSessionWebSocketTask._( + _nsObject.webSocketTaskWithURL_protocols_( + uriToNSURL(uri), stringIterableToNSArray(protocols))); + } + _setupDelegation(_delegate, this, task, + onComplete: _onComplete, + onData: _onData, + onFinishedDownloading: _onFinishedDownloading, + onRedirect: _onRedirect, + onResponse: _onResponse, + onWebSocketTaskOpened: _onWebSocketTaskOpened, + onWebSocketTaskClosed: _onWebSocketTaskClosed); + return task; + } } diff --git a/pkgs/cupertino_http/lib/src/cupertino_web_socket.dart b/pkgs/cupertino_http/lib/src/cupertino_web_socket.dart new file mode 100644 index 0000000000..6b4cab070c --- /dev/null +++ b/pkgs/cupertino_http/lib/src/cupertino_web_socket.dart @@ -0,0 +1,178 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:web_socket/web_socket.dart'; + +import 'cupertino_api.dart'; + +/// An error occurred while connecting to the peer. +class ConnectionErrorException extends WebSocketException { + final Error error; + + ConnectionErrorException(super.message, this.error); + + @override + String toString() => 'CupertinoErrorWebSocketException: $message $error'; +} + +/// A [WebSocket] using the +/// [NSURLSessionWebSocketTask API](https://developer.apple.com/documentation/foundation/nsurlsessionwebsockettask). +class CupertinoWebSocket implements WebSocket { + /// Create a new WebSocket connection using the + /// [NSURLSessionWebSocketTask API](https://developer.apple.com/documentation/foundation/nsurlsessionwebsockettask). + /// + /// The URL supplied in [url] must use the scheme ws or wss. + /// + /// 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). + static Future connect(Uri url, + {Iterable? protocols}) async { + if (!url.isScheme('ws') && !url.isScheme('wss')) { + throw ArgumentError.value( + url, 'url', 'only ws: and wss: schemes are supported'); + } + + final readyCompleter = Completer(); + late CupertinoWebSocket webSocket; + + final session = URLSession.sessionWithConfiguration( + URLSessionConfiguration.defaultSessionConfiguration(), + onComplete: (session, task, error) { + if (!readyCompleter.isCompleted) { + if (error != null) { + readyCompleter.completeError( + ConnectionErrorException('connection ended unexpectedly', error)); + } else { + webSocket = CupertinoWebSocket._(task as URLSessionWebSocketTask, ''); + readyCompleter.complete(webSocket); + } + } else { + webSocket._connectionClosed( + 1006, Data.fromList('abnormal close'.codeUnits)); + } + }, onWebSocketTaskOpened: (session, task, protocol) { + webSocket = CupertinoWebSocket._(task, protocol ?? ''); + readyCompleter.complete(webSocket); + }, onWebSocketTaskClosed: (session, task, closeCode, reason) { + webSocket._connectionClosed(closeCode, reason); + }); + + session.webSocketTaskWithURL(url, protocols: protocols).resume(); + return readyCompleter.future; + } + + final URLSessionWebSocketTask _task; + final String _protocol; + final _events = StreamController(); + + CupertinoWebSocket._(this._task, this._protocol) { + _scheduleReceive(); + } + + /// Handle an incoming message from the peer and schedule receiving the next + /// message. + void _handleMessage(URLSessionWebSocketMessage value) { + late WebSocketEvent event; + switch (value.type) { + case URLSessionWebSocketMessageType.urlSessionWebSocketMessageTypeString: + event = TextDataReceived(value.string!); + break; + case URLSessionWebSocketMessageType.urlSessionWebSocketMessageTypeData: + event = BinaryDataReceived(value.data!.bytes); + break; + } + _events.add(event); + _scheduleReceive(); + } + + void _scheduleReceive() { + unawaited(_task + .receiveMessage() + .then(_handleMessage, onError: _closeConnectionWithError)); + } + + /// Close the WebSocket connection due to an error and send the + /// [CloseReceived] event. + void _closeConnectionWithError(Object e) { + if (e is Error) { + if (e.domain == 'NSPOSIXErrorDomain' && e.code == 57) { + // Socket is not connected. + // onWebSocketTaskClosed/onComplete will be invoked and may indicate a + // close code. + return; + } + var (int code, String? reason) = switch ([e.domain, e.code]) { + ['NSPOSIXErrorDomain', 100] => (1002, e.localizedDescription), + _ => (1006, e.localizedDescription) + }; + _task.cancel(); + _connectionClosed( + code, reason == null ? null : Data.fromList(reason.codeUnits)); + } else { + throw StateError('unexpected error: $e'); + } + } + + void _connectionClosed(int? closeCode, Data? reason) { + if (!_events.isClosed) { + final closeReason = reason == null ? '' : utf8.decode(reason.bytes); + + _events + ..add(CloseReceived(closeCode, closeReason)) + ..close(); + } + } + + @override + void sendBytes(Uint8List b) { + if (_events.isClosed) { + throw StateError('WebSocket is closed'); + } + _task + .sendMessage(URLSessionWebSocketMessage.fromData(Data.fromList(b))) + .then((_) => _, onError: _closeConnectionWithError); + } + + @override + void sendText(String s) { + if (_events.isClosed) { + throw StateError('WebSocket is closed'); + } + _task + .sendMessage(URLSessionWebSocketMessage.fromString(s)) + .then((_) => _, onError: _closeConnectionWithError); + } + + @override + Future close([int? code, String? reason]) async { + if (_events.isClosed) { + throw StateError('WebSocket is closed'); + } + + if (code != null) { + RangeError.checkValueInInterval(code, 3000, 4999, 'code'); + } + if (reason != null && utf8.encode(reason).length > 123) { + throw ArgumentError.value(reason, 'reason', + 'reason must be <= 123 bytes long when encoded as UTF-8'); + } + + if (!_events.isClosed) { + unawaited(_events.close()); + if (code != null) { + reason = reason ?? ''; + _task.cancelWithCloseCode(code, Data.fromList(reason.codeUnits)); + } else { + _task.cancel(); + } + } + } + + @override + Stream get events => _events.stream; + + @override + String get protocol => _protocol; +} diff --git a/pkgs/cupertino_http/lib/src/utils.dart b/pkgs/cupertino_http/lib/src/utils.dart index be23e5cdcb..02fc5489b1 100644 --- a/pkgs/cupertino_http/lib/src/utils.dart +++ b/pkgs/cupertino_http/lib/src/utils.dart @@ -59,7 +59,7 @@ String? toStringOrNull(ncb.NSString? s) { /// Converts a NSDictionary containing NSString keys and NSString values into /// an equivalent map. -Map stringDictToMap(ncb.NSDictionary d) { +Map stringNSDictionaryToMap(ncb.NSDictionary d) { // TODO(https://github.com/dart-lang/ffigen/issues/374): Make this // function type safe. Currently it will unconditionally cast both keys and // values to NSString with a likely crash down the line if that isn't their @@ -78,5 +78,16 @@ Map stringDictToMap(ncb.NSDictionary d) { return m; } +ncb.NSArray stringIterableToNSArray(Iterable strings) { + final array = + ncb.NSMutableArray.arrayWithCapacity_(linkedLibs, strings.length); + + var index = 0; + for (var s in strings) { + array.setObject_atIndexedSubscript_(s.toNSString(linkedLibs), index++); + } + return array; +} + ncb.NSURL uriToNSURL(Uri uri) => ncb.NSURL.URLWithString_(linkedLibs, uri.toString().toNSString(linkedLibs)); diff --git a/pkgs/cupertino_http/pubspec.yaml b/pkgs/cupertino_http/pubspec.yaml index 2a819e120b..13255127b8 100644 --- a/pkgs/cupertino_http/pubspec.yaml +++ b/pkgs/cupertino_http/pubspec.yaml @@ -15,6 +15,8 @@ dependencies: flutter: sdk: flutter http: ^1.2.0 + web_socket: + path: ../web_socket dev_dependencies: dart_flutter_team_lints: ^2.0.0 From 3a43cf10b7727f99055e7c6690d766bebcfcf9af Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Thu, 7 Mar 2024 18:25:37 -0800 Subject: [PATCH 02/11] Update versions --- .github/workflows/cupertino.yml | 2 +- pkgs/cupertino_http/pubspec.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cupertino.yml b/.github/workflows/cupertino.yml index f434431b44..9b470e1fea 100644 --- a/.github/workflows/cupertino.yml +++ b/.github/workflows/cupertino.yml @@ -31,7 +31,7 @@ jobs: matrix: # Test on the minimum supported flutter version and the latest # version. - flutter-version: ["3.16.0", "any"] + flutter-version: ["3.19.0", "any"] steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - uses: subosito/flutter-action@2783a3f08e1baf891508463f8c6653c258246225 diff --git a/pkgs/cupertino_http/pubspec.yaml b/pkgs/cupertino_http/pubspec.yaml index 13255127b8..fb894015d2 100644 --- a/pkgs/cupertino_http/pubspec.yaml +++ b/pkgs/cupertino_http/pubspec.yaml @@ -6,8 +6,8 @@ description: >- repository: https://github.com/dart-lang/http/tree/master/pkgs/cupertino_http environment: - sdk: ^3.2.0 - flutter: '>=3.16.0' # If changed, update test matrix. + sdk: ^3.3.0 + flutter: '>=3.19.0' # If changed, update test matrix. dependencies: async: ^2.5.0 From cc7214da3543268f859ad4652a1ac9ef295afbed Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Thu, 7 Mar 2024 18:29:04 -0800 Subject: [PATCH 03/11] Update pubspec.yaml --- pkgs/cupertino_http/pubspec.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkgs/cupertino_http/pubspec.yaml b/pkgs/cupertino_http/pubspec.yaml index fb894015d2..bbbddb761d 100644 --- a/pkgs/cupertino_http/pubspec.yaml +++ b/pkgs/cupertino_http/pubspec.yaml @@ -1,5 +1,7 @@ name: cupertino_http version: 1.3.1-wip +publish_to: none # Remove this! + description: >- A macOS/iOS Flutter plugin that provides access to the Foundation URL Loading System. From f44d5e3aa8a02c40ccd16ef83b07eb3ecedfdc6e Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Thu, 7 Mar 2024 18:36:52 -0800 Subject: [PATCH 04/11] Copyright --- pkgs/cupertino_http/lib/src/cupertino_web_socket.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkgs/cupertino_http/lib/src/cupertino_web_socket.dart b/pkgs/cupertino_http/lib/src/cupertino_web_socket.dart index 6b4cab070c..33dc04a7f6 100644 --- a/pkgs/cupertino_http/lib/src/cupertino_web_socket.dart +++ b/pkgs/cupertino_http/lib/src/cupertino_web_socket.dart @@ -1,3 +1,7 @@ +// 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:convert'; import 'dart:typed_data'; From 5873182ec475dad2dd4bb36d943ad9233a860fb1 Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Thu, 7 Mar 2024 18:38:41 -0800 Subject: [PATCH 05/11] Update cupertino_web_socket.dart --- pkgs/cupertino_http/lib/src/cupertino_web_socket.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkgs/cupertino_http/lib/src/cupertino_web_socket.dart b/pkgs/cupertino_http/lib/src/cupertino_web_socket.dart index 33dc04a7f6..dbd0195d4c 100644 --- a/pkgs/cupertino_http/lib/src/cupertino_web_socket.dart +++ b/pkgs/cupertino_http/lib/src/cupertino_web_socket.dart @@ -11,10 +11,10 @@ import 'package:web_socket/web_socket.dart'; import 'cupertino_api.dart'; /// An error occurred while connecting to the peer. -class ConnectionErrorException extends WebSocketException { +class ConnectionException extends WebSocketException { final Error error; - ConnectionErrorException(super.message, this.error); + ConnectionException(super.message, this.error); @override String toString() => 'CupertinoErrorWebSocketException: $message $error'; @@ -47,7 +47,7 @@ class CupertinoWebSocket implements WebSocket { if (!readyCompleter.isCompleted) { if (error != null) { readyCompleter.completeError( - ConnectionErrorException('connection ended unexpectedly', error)); + ConnectionException('connection ended unexpectedly', error)); } else { webSocket = CupertinoWebSocket._(task as URLSessionWebSocketTask, ''); readyCompleter.complete(webSocket); From 98c0ae7ac096664e16a7b67b5f13cdcb07759540 Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Fri, 8 Mar 2024 12:07:40 -0800 Subject: [PATCH 06/11] Update pubspec.yaml --- pkgs/cupertino_http/pubspec.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkgs/cupertino_http/pubspec.yaml b/pkgs/cupertino_http/pubspec.yaml index bbbddb761d..9d375981d0 100644 --- a/pkgs/cupertino_http/pubspec.yaml +++ b/pkgs/cupertino_http/pubspec.yaml @@ -1,6 +1,6 @@ name: cupertino_http -version: 1.3.1-wip -publish_to: none # Remove this! +version: 1.4.0-wip +publish_to: none # Do not merge with this here! description: >- A macOS/iOS Flutter plugin that provides access to the Foundation URL From 34d390808fce67061a42a7a67df11cd2e37408d9 Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Mon, 11 Mar 2024 10:34:19 -0700 Subject: [PATCH 07/11] Fix --- .github/workflows/cupertino.yml | 2 +- .../example/integration_test/main.dart | 2 ++ .../web_socket_conformance_test.dart | 12 ++++++++++++ .../cupertino_http/lib/src/cupertino_web_socket.dart | 7 ++++--- 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/.github/workflows/cupertino.yml b/.github/workflows/cupertino.yml index 9b470e1fea..1110e60e08 100644 --- a/.github/workflows/cupertino.yml +++ b/.github/workflows/cupertino.yml @@ -61,4 +61,4 @@ jobs: run: | cd example flutter pub get - flutter test --timeout=1200s integration_test/ + flutter test --timeout=1200s integration_test/main.dart diff --git a/pkgs/cupertino_http/example/integration_test/main.dart b/pkgs/cupertino_http/example/integration_test/main.dart index 12128b5b9a..0d4d5e16d9 100644 --- a/pkgs/cupertino_http/example/integration_test/main.dart +++ b/pkgs/cupertino_http/example/integration_test/main.dart @@ -19,6 +19,7 @@ import 'url_session_delegate_test.dart' as url_session_delegate_test; import 'url_session_task_test.dart' as url_session_task_test; import 'url_session_test.dart' as url_session_test; import 'utils_test.dart' as utils_test; +import 'web_socket_conformance_test.dart' as web_socket_conformance_test; /// Execute all the tests in this directory. /// @@ -43,4 +44,5 @@ void main() { url_session_task_test.main(); url_session_test.main(); utils_test.main(); + web_socket_conformance_test.main(); } diff --git a/pkgs/cupertino_http/example/integration_test/web_socket_conformance_test.dart b/pkgs/cupertino_http/example/integration_test/web_socket_conformance_test.dart index 68e5f80322..8dff3a2626 100644 --- a/pkgs/cupertino_http/example/integration_test/web_socket_conformance_test.dart +++ b/pkgs/cupertino_http/example/integration_test/web_socket_conformance_test.dart @@ -3,8 +3,20 @@ // BSD-style license that can be found in the LICENSE file. import 'package:cupertino_http/cupertino_http.dart'; +import 'package:test/test.dart'; import 'package:web_socket_conformance_tests/web_socket_conformance_tests.dart'; void main() { testAll(CupertinoWebSocket.connect); + + group('defaultSessionConfiguration', () { + testAll( + CupertinoWebSocket.connect, + ); + }); + group('fromSessionConfiguration', () { + final config = URLSessionConfiguration.ephemeralSessionConfiguration(); + testAll((uri, {protocols}) => + CupertinoWebSocket.connect(uri, protocols: protocols, config: config)); + }); } diff --git a/pkgs/cupertino_http/lib/src/cupertino_web_socket.dart b/pkgs/cupertino_http/lib/src/cupertino_web_socket.dart index dbd0195d4c..14fa40ed14 100644 --- a/pkgs/cupertino_http/lib/src/cupertino_web_socket.dart +++ b/pkgs/cupertino_http/lib/src/cupertino_web_socket.dart @@ -20,7 +20,7 @@ class ConnectionException extends WebSocketException { String toString() => 'CupertinoErrorWebSocketException: $message $error'; } -/// A [WebSocket] using the +/// A [WebSocket] implemented using the /// [NSURLSessionWebSocketTask API](https://developer.apple.com/documentation/foundation/nsurlsessionwebsockettask). class CupertinoWebSocket implements WebSocket { /// Create a new WebSocket connection using the @@ -32,7 +32,8 @@ class CupertinoWebSocket implements WebSocket { /// the peer is able to select. See /// [RFC-6455 1.9](https://datatracker.ietf.org/doc/html/rfc6455#section-1.9). static Future connect(Uri url, - {Iterable? protocols}) async { + {Iterable? protocols, + URLSessionConfiguration? config}) async { if (!url.isScheme('ws') && !url.isScheme('wss')) { throw ArgumentError.value( url, 'url', 'only ws: and wss: schemes are supported'); @@ -42,7 +43,7 @@ class CupertinoWebSocket implements WebSocket { late CupertinoWebSocket webSocket; final session = URLSession.sessionWithConfiguration( - URLSessionConfiguration.defaultSessionConfiguration(), + config ?? URLSessionConfiguration.defaultSessionConfiguration(), onComplete: (session, task, error) { if (!readyCompleter.isCompleted) { if (error != null) { From 6f32092b4af4af389cd4330b7110d5cbbfb939b8 Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Mon, 11 Mar 2024 10:46:10 -0700 Subject: [PATCH 08/11] Format fix --- pkgs/cupertino_http/lib/src/cupertino_web_socket.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkgs/cupertino_http/lib/src/cupertino_web_socket.dart b/pkgs/cupertino_http/lib/src/cupertino_web_socket.dart index 14fa40ed14..2377bc0442 100644 --- a/pkgs/cupertino_http/lib/src/cupertino_web_socket.dart +++ b/pkgs/cupertino_http/lib/src/cupertino_web_socket.dart @@ -32,8 +32,7 @@ class CupertinoWebSocket implements WebSocket { /// the peer is able to select. See /// [RFC-6455 1.9](https://datatracker.ietf.org/doc/html/rfc6455#section-1.9). static Future connect(Uri url, - {Iterable? protocols, - URLSessionConfiguration? config}) async { + {Iterable? protocols, URLSessionConfiguration? config}) async { if (!url.isScheme('ws') && !url.isScheme('wss')) { throw ArgumentError.value( url, 'url', 'only ws: and wss: schemes are supported'); From e1ada6dd2818437238a23ccf857a9e67f6554d2f Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Mon, 11 Mar 2024 15:33:48 -0700 Subject: [PATCH 09/11] Comments --- .../lib/src/cupertino_web_socket.dart | 46 ++++++++++++++----- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/pkgs/cupertino_http/lib/src/cupertino_web_socket.dart b/pkgs/cupertino_http/lib/src/cupertino_web_socket.dart index 2377bc0442..23ad7bfe1a 100644 --- a/pkgs/cupertino_http/lib/src/cupertino_web_socket.dart +++ b/pkgs/cupertino_http/lib/src/cupertino_web_socket.dart @@ -43,24 +43,46 @@ class CupertinoWebSocket implements WebSocket { final session = URLSession.sessionWithConfiguration( config ?? URLSessionConfiguration.defaultSessionConfiguration(), - onComplete: (session, task, error) { + // In a successful flow, the callbacks will be made in this order: + // onWebSocketTaskOpened(...) // Good connect. + // + // onWebSocketTaskClosed(...) // Optional: peer sent Close frame. + // onComplete(..., error=null) // Disconnected. + // + // In a failure to connect to the peer, the flow will be: + // onComplete(session, task, error=error): + // + // `onComplete` can also be called at any point if the peer is + // disconnected without Close frames being exchanged. + onWebSocketTaskOpened: (session, task, protocol) { + webSocket = CupertinoWebSocket._(task, protocol ?? ''); + readyCompleter.complete(webSocket); + }, onWebSocketTaskClosed: (session, task, closeCode, reason) { + assert(readyCompleter.isCompleted); + webSocket._connectionClosed(closeCode, reason); + }, onComplete: (session, task, error) { if (!readyCompleter.isCompleted) { - if (error != null) { - readyCompleter.completeError( - ConnectionException('connection ended unexpectedly', error)); - } else { - webSocket = CupertinoWebSocket._(task as URLSessionWebSocketTask, ''); - readyCompleter.complete(webSocket); + // `onWebSocketTaskOpened should have been called and completed + // `readyCompleter`. So either there was a error creating the connection + // or a logic error. + if (error == null) { + throw AssertionError( + 'expected an error or "onWebSocketTaskOpened" to be called ' + 'first'); } + readyCompleter.completeError( + ConnectionException('connection ended unexpectedly', error)); } else { + // There are three possibilities here: + // 1. the peer sent a close Frame, `onWebSocketTaskClosed` was already + // called and `_connectionClosed` is a no-op. + // 2. we sent a close Frame (through `close()`) and `_connectionClosed` + // is a no-op. + // 3. an error occured (e.g. network failure) and `_connectionClosed` + // will signal that and close `event`. webSocket._connectionClosed( 1006, Data.fromList('abnormal close'.codeUnits)); } - }, onWebSocketTaskOpened: (session, task, protocol) { - webSocket = CupertinoWebSocket._(task, protocol ?? ''); - readyCompleter.complete(webSocket); - }, onWebSocketTaskClosed: (session, task, closeCode, reason) { - webSocket._connectionClosed(closeCode, reason); }); session.webSocketTaskWithURL(url, protocols: protocols).resume(); From 5605c0e9acdda5710d0dfd838c0c5658ced3e2f4 Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Tue, 19 Mar 2024 13:07:29 -0700 Subject: [PATCH 10/11] Debug print --- pkgs/cupertino_http/lib/src/cupertino_web_socket.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkgs/cupertino_http/lib/src/cupertino_web_socket.dart b/pkgs/cupertino_http/lib/src/cupertino_web_socket.dart index 23ad7bfe1a..a626568fb0 100644 --- a/pkgs/cupertino_http/lib/src/cupertino_web_socket.dart +++ b/pkgs/cupertino_http/lib/src/cupertino_web_socket.dart @@ -189,7 +189,8 @@ class CupertinoWebSocket implements WebSocket { unawaited(_events.close()); if (code != null) { reason = reason ?? ''; - _task.cancelWithCloseCode(code, Data.fromList(reason.codeUnits)); + print('Closing with code: $code $reason'); + _task.cancelWithCloseCode(code, Data.fromList(utf8.encode(reason))); } else { _task.cancel(); } From 1aa6aa882ce301365361b09de2cf970a2f2252eb Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Tue, 19 Mar 2024 13:48:06 -0700 Subject: [PATCH 11/11] Remove print --- pkgs/cupertino_http/lib/src/cupertino_web_socket.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/pkgs/cupertino_http/lib/src/cupertino_web_socket.dart b/pkgs/cupertino_http/lib/src/cupertino_web_socket.dart index a626568fb0..94246744c2 100644 --- a/pkgs/cupertino_http/lib/src/cupertino_web_socket.dart +++ b/pkgs/cupertino_http/lib/src/cupertino_web_socket.dart @@ -189,7 +189,6 @@ class CupertinoWebSocket implements WebSocket { unawaited(_events.close()); if (code != null) { reason = reason ?? ''; - print('Closing with code: $code $reason'); _task.cancelWithCloseCode(code, Data.fromList(utf8.encode(reason))); } else { _task.cancel();