From 784bf00125fa74735eb980755a2b761581df902a Mon Sep 17 00:00:00 2001 From: Elliot Sayes Date: Thu, 2 Mar 2023 23:19:25 +0800 Subject: [PATCH 01/11] Implement+test getAsByteStream with `package:http` --- lib/src/ardrive_http.dart | 13 +++++++++++++ pubspec.yaml | 2 ++ test/ardrive_http_test.dart | 11 +++++++++++ 3 files changed, 26 insertions(+) diff --git a/lib/src/ardrive_http.dart b/lib/src/ardrive_http.dart index 280f04e..4984cbd 100644 --- a/lib/src/ardrive_http.dart +++ b/lib/src/ardrive_http.dart @@ -7,6 +7,7 @@ import 'package:ardrive_http/src/responses.dart'; import 'package:dio/dio.dart'; import 'package:dio_smart_retry/dio_smart_retry.dart'; import 'package:flutter/foundation.dart'; +import 'package:http/http.dart' as http; import 'package:isolated_worker/js_isolated_worker.dart'; const List jsScriptsToImport = ['ardrive-http.js']; @@ -87,6 +88,18 @@ class ArDriveHTTP { return get(url: url, responseType: ResponseType.bytes); } + Future getAsByteStream(String url) async { + final client = http.Client(); + final response = await client.send(http.Request('GET', Uri.parse(url))); + final byteStream = response.stream.map((event) => Uint8List.fromList(event)); + return ArDriveHTTPResponse( + data: byteStream, + statusCode: response.statusCode, + statusMessage: response.reasonPhrase, + retryAttempts: retryAttempts, + ); + } + Future _getIO(Map params) async { final String url = params['url']; final ResponseType responseType = params['responseType']; diff --git a/pubspec.yaml b/pubspec.yaml index 374c1c2..7a5f358 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -26,6 +26,7 @@ dependencies: equatable: ^2.0.5 flutter: sdk: flutter + http: ^0.13.5 isolated_worker: ^0.1.1 shelf: ^1.4.0 shelf_router: ^1.1.3 @@ -34,3 +35,4 @@ dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^2.0.1 + async: ^2.9.0 diff --git a/test/ardrive_http_test.dart b/test/ardrive_http_test.dart index 453230f..9e2700a 100644 --- a/test/ardrive_http_test.dart +++ b/test/ardrive_http_test.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:ardrive_http/ardrive_http.dart'; +import 'package:async/async.dart'; import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -62,6 +63,16 @@ void main() { expect(getAsBytesResponse.retryAttempts, 0); }); + test('returns byte stream response', () async { + const String url = '$baseUrl/ok'; + + final getResponse = await http.getAsByteStream(url); + final byteStream = getResponse.data as Stream; + + expect(collectBytes(byteStream), completion(Uint8List.fromList([111, 107]))); + expect(getResponse.retryAttempts, 0); + }); + test('fail without retry', () async { const String url = '$baseUrl/404'; From e530b369f5c7391fb5c041c047cfdf93d0616267 Mon Sep 17 00:00:00 2001 From: Elliot Sayes Date: Thu, 2 Mar 2023 23:33:47 +0800 Subject: [PATCH 02/11] Use `package:fetch_stream` on web --- lib/src/ardrive_http.dart | 10 ++++++++-- pubspec.yaml | 1 + 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/src/ardrive_http.dart b/lib/src/ardrive_http.dart index 4984cbd..dc82a46 100644 --- a/lib/src/ardrive_http.dart +++ b/lib/src/ardrive_http.dart @@ -6,6 +6,7 @@ import 'dart:math'; import 'package:ardrive_http/src/responses.dart'; import 'package:dio/dio.dart'; import 'package:dio_smart_retry/dio_smart_retry.dart'; +import 'package:fetch_client/fetch_client.dart'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'package:isolated_worker/js_isolated_worker.dart'; @@ -89,8 +90,13 @@ class ArDriveHTTP { } Future getAsByteStream(String url) async { - final client = http.Client(); - final response = await client.send(http.Request('GET', Uri.parse(url))); + final client = kIsWeb? FetchClient() : http.Client(); + final response = await client.send( + http.Request( + 'GET', + Uri.parse(url), + ), + ); final byteStream = response.stream.map((event) => Uint8List.fromList(event)); return ArDriveHTTPResponse( data: byteStream, diff --git a/pubspec.yaml b/pubspec.yaml index 7a5f358..62e137a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,6 +24,7 @@ dependencies: dio: ^5.0.0 dio_smart_retry: ^5.0.0 equatable: ^2.0.5 + fetch_client: ^1.0.0-dev.2 flutter: sdk: flutter http: ^0.13.5 From 6b96fe7f4b44b1996b72a667af8238c694988397 Mon Sep 17 00:00:00 2001 From: Elliot Sayes Date: Mon, 6 Mar 2023 14:33:54 +0800 Subject: [PATCH 03/11] Bump dependencies --- pubspec.yaml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index 62e137a..c84fc87 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,7 +5,7 @@ publish_to: none environment: sdk: '>=2.18.5 <3.0.0' - flutter: 3.3.9 + flutter: 3.7.0 script_runner: shell: @@ -24,7 +24,7 @@ dependencies: dio: ^5.0.0 dio_smart_retry: ^5.0.0 equatable: ^2.0.5 - fetch_client: ^1.0.0-dev.2 + fetch_client: ^1.0.0 flutter: sdk: flutter http: ^0.13.5 @@ -37,3 +37,11 @@ dev_dependencies: sdk: flutter flutter_lints: ^2.0.1 async: ^2.9.0 + +# `fetch_api` from `fetch_client` specifies `js: ^0.6.7`, however +# this is incompatible with `js: ^0.6.5` from Flutter SDK `3.7.0` +# Issue: https://github.com/Zekfad/fetch_api/pull/2 + +# Workaround: solves the dependency conflict by using the newer version. +dependency_overrides: + js: ^0.6.7 From ef2dbd44e32fe7ef990127ee7bb006795e98ce1e Mon Sep 17 00:00:00 2001 From: Elliot Sayes Date: Mon, 6 Mar 2023 14:34:32 +0800 Subject: [PATCH 04/11] Use stub on IO & fix cors --- lib/src/ardrive_http.dart | 8 ++- lib/src/io/fetch_client_stub.dart | 81 +++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 lib/src/io/fetch_client_stub.dart diff --git a/lib/src/ardrive_http.dart b/lib/src/ardrive_http.dart index dc82a46..b5817f5 100644 --- a/lib/src/ardrive_http.dart +++ b/lib/src/ardrive_http.dart @@ -6,11 +6,13 @@ import 'dart:math'; import 'package:ardrive_http/src/responses.dart'; import 'package:dio/dio.dart'; import 'package:dio_smart_retry/dio_smart_retry.dart'; -import 'package:fetch_client/fetch_client.dart'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'package:isolated_worker/js_isolated_worker.dart'; +import 'io/fetch_client_stub.dart' + if (dart.library.html) 'package:fetch_client/fetch_client.dart' as fetch; + const List jsScriptsToImport = ['ardrive-http.js']; String normalizeResponseTypeToJS(ResponseType responseType) { @@ -90,7 +92,9 @@ class ArDriveHTTP { } Future getAsByteStream(String url) async { - final client = kIsWeb? FetchClient() : http.Client(); + final client = kIsWeb + ? fetch.FetchClient(mode: fetch.RequestMode.cors) + : http.Client(); final response = await client.send( http.Request( 'GET', diff --git a/lib/src/io/fetch_client_stub.dart b/lib/src/io/fetch_client_stub.dart new file mode 100644 index 0000000..5b0bf84 --- /dev/null +++ b/lib/src/io/fetch_client_stub.dart @@ -0,0 +1,81 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:http/http.dart'; + +enum RequestMode { + sameOrigin('same-origin'), + noCors('no-cors'), + cors('cors'), + navigate('navigate'), + webSocket('websocket'); + + const RequestMode(this.mode); + + factory RequestMode.from(String mode) => + values.firstWhere((element) => element.mode == mode); + + final String mode; + + @override + String toString() => mode; +} + + +class FetchClient implements BaseClient { + FetchClient({ + RequestMode? mode, + }) { + throw UnimplementedError(); + } + + @override + Future send(BaseRequest request) { + throw UnimplementedError(); + } + + @override + void close() { + throw UnimplementedError(); + } + + @override + Future delete(Uri url, {Map? headers, Object? body, Encoding? encoding}) { + throw UnimplementedError(); + } + + @override + Future get(Uri url, {Map? headers}) { + throw UnimplementedError(); + } + + @override + Future head(Uri url, {Map? headers}) { + throw UnimplementedError(); + } + + @override + Future patch(Uri url, {Map? headers, Object? body, Encoding? encoding}) { + throw UnimplementedError(); + } + + @override + Future post(Uri url, {Map? headers, Object? body, Encoding? encoding}) { + throw UnimplementedError(); + } + + @override + Future put(Uri url, {Map? headers, Object? body, Encoding? encoding}) { + throw UnimplementedError(); + } + + @override + Future read(Uri url, {Map? headers}) { + throw UnimplementedError(); + } + + @override + Future readBytes(Uri url, {Map? headers}) { + throw UnimplementedError(); + } +} From c2b7f3dcbe531d6d7588bb1915ad25ef72972d6b Mon Sep 17 00:00:00 2001 From: Elliot Sayes Date: Fri, 17 Mar 2023 23:25:59 +0800 Subject: [PATCH 05/11] Remove dependency override (fixed in fetch_api 1.0.1) --- pubspec.yaml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index c84fc87..66f8e8c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -37,11 +37,3 @@ dev_dependencies: sdk: flutter flutter_lints: ^2.0.1 async: ^2.9.0 - -# `fetch_api` from `fetch_client` specifies `js: ^0.6.7`, however -# this is incompatible with `js: ^0.6.5` from Flutter SDK `3.7.0` -# Issue: https://github.com/Zekfad/fetch_api/pull/2 - -# Workaround: solves the dependency conflict by using the newer version. -dependency_overrides: - js: ^0.6.7 From 59821eda86811166d9932dbeee0b8e4bb5266b8a Mon Sep 17 00:00:00 2001 From: Elliot Sayes Date: Mon, 20 Mar 2023 09:22:22 +0800 Subject: [PATCH 06/11] Update readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 64e5730..a96ae85 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,8 @@ ArDriveHTTP is a package to perform network calls for ArDrive Web. It uses Isola - get() - getJson() - getAsBytes() +- getAsByteStream() + - Note: does not use Isolates or WebWorkers ## Getting started From 5cf496501f6695409a5c04c043086164865890c4 Mon Sep 17 00:00:00 2001 From: Elliot Sayes Date: Mon, 20 Mar 2023 14:53:53 +0800 Subject: [PATCH 07/11] Add support for cancelling on web --- lib/src/ardrive_http.dart | 42 ++++++++++++++++++++++++++++--- lib/src/io/fetch_client_stub.dart | 11 +++++++- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/lib/src/ardrive_http.dart b/lib/src/ardrive_http.dart index b5817f5..cc15266 100644 --- a/lib/src/ardrive_http.dart +++ b/lib/src/ardrive_http.dart @@ -91,16 +91,50 @@ class ArDriveHTTP { return get(url: url, responseType: ResponseType.bytes); } - Future getAsByteStream(String url) async { - final client = kIsWeb - ? fetch.FetchClient(mode: fetch.RequestMode.cors) - : http.Client(); + Future getAsByteStream(String url, {Completer? cancelWithReason}) async { + return kIsWeb + ? _getAsByteStreamWeb(url, cancelWithReason: cancelWithReason) + : _getAsByteStreamIO(url, cancelWithReason: cancelWithReason); + } + + Future _getAsByteStreamWeb(String url, {Completer? cancelWithReason}) async { + final client = fetch.FetchClient(mode: fetch.RequestMode.cors); + + final response = await client.send( + http.Request( + 'GET', + Uri.parse(url), + ), + ); + + cancelWithReason?.future.then((value) { + debugPrint('Cancelling request to $url with reason: $value'); + response.cancel(); + }); + + final byteStream = response.stream.map((event) => Uint8List.fromList(event)); + return ArDriveHTTPResponse( + data: byteStream, + statusCode: response.statusCode, + statusMessage: response.reasonPhrase, + retryAttempts: retryAttempts, + ); + } + + Future _getAsByteStreamIO(String url, {Completer? cancelWithReason}) async { + if (cancelWithReason != null) { + debugPrint('Warning: Canceling requests is not supported on the IO platform'); + } + + final client = http.Client(); + final response = await client.send( http.Request( 'GET', Uri.parse(url), ), ); + final byteStream = response.stream.map((event) => Uint8List.fromList(event)); return ArDriveHTTPResponse( data: byteStream, diff --git a/lib/src/io/fetch_client_stub.dart b/lib/src/io/fetch_client_stub.dart index 5b0bf84..c2f1acb 100644 --- a/lib/src/io/fetch_client_stub.dart +++ b/lib/src/io/fetch_client_stub.dart @@ -21,6 +21,15 @@ enum RequestMode { String toString() => mode; } +class FetchResponse extends StreamedResponse { + FetchResponse(super.stream, super.statusCode, this.cancel, this.url, this.redirected); + + final void Function() cancel; + + final String url; + + final bool redirected; +} class FetchClient implements BaseClient { FetchClient({ @@ -30,7 +39,7 @@ class FetchClient implements BaseClient { } @override - Future send(BaseRequest request) { + Future send(BaseRequest request) { throw UnimplementedError(); } From 3bcb733f3b1c10c597741066542ecfdcf303dd40 Mon Sep 17 00:00:00 2001 From: Elliot Sayes Date: Mon, 20 Mar 2023 17:06:19 +0800 Subject: [PATCH 08/11] Abort on IO with `cancellation_token_http` lib --- lib/src/ardrive_http.dart | 15 +++++++++------ pubspec.yaml | 1 + 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/src/ardrive_http.dart b/lib/src/ardrive_http.dart index cc15266..0a0314b 100644 --- a/lib/src/ardrive_http.dart +++ b/lib/src/ardrive_http.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'dart:math'; import 'package:ardrive_http/src/responses.dart'; +import 'package:cancellation_token_http/http.dart' as http_cancel; import 'package:dio/dio.dart'; import 'package:dio_smart_retry/dio_smart_retry.dart'; import 'package:flutter/foundation.dart'; @@ -122,19 +123,21 @@ class ArDriveHTTP { } Future _getAsByteStreamIO(String url, {Completer? cancelWithReason}) async { - if (cancelWithReason != null) { - debugPrint('Warning: Canceling requests is not supported on the IO platform'); - } - - final client = http.Client(); + final client = http_cancel.Client(); + final cancellationToken = http_cancel.CancellationToken(); final response = await client.send( - http.Request( + http_cancel.Request( 'GET', Uri.parse(url), ), ); + cancelWithReason?.future.then((value) { + debugPrint('Cancelling request to $url with reason: $value'); + cancellationToken.cancel(); + }); + final byteStream = response.stream.map((event) => Uint8List.fromList(event)); return ArDriveHTTPResponse( data: byteStream, diff --git a/pubspec.yaml b/pubspec.yaml index 66f8e8c..7901f76 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -21,6 +21,7 @@ script_runner: - test: scr test-vm && scr test-web dependencies: + cancellation_token_http: ^1.2.0 dio: ^5.0.0 dio_smart_retry: ^5.0.0 equatable: ^2.0.5 From 29373b73e5d1ef32111b2766cdd9dd3ab02dd375 Mon Sep 17 00:00:00 2001 From: Elliot Sayes Date: Wed, 22 Mar 2023 07:48:52 +0800 Subject: [PATCH 09/11] Fix missing cancellationToken (oops) --- lib/src/ardrive_http.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/ardrive_http.dart b/lib/src/ardrive_http.dart index 0a0314b..c0fe788 100644 --- a/lib/src/ardrive_http.dart +++ b/lib/src/ardrive_http.dart @@ -131,6 +131,7 @@ class ArDriveHTTP { 'GET', Uri.parse(url), ), + cancellationToken: cancellationToken, ); cancelWithReason?.future.then((value) { From 4b6a5e1ad185f273446299d3cb3a2c0030087333 Mon Sep 17 00:00:00 2001 From: Elliot Sayes Date: Wed, 22 Mar 2023 09:02:28 +0800 Subject: [PATCH 10/11] Add retries to bytestream --- lib/src/ardrive_http.dart | 34 ++++++++++++++++++++++++++-------- lib/src/utils.dart | 21 +++++++++++++++++++++ test/ardrive_http_test.dart | 3 +-- test/webserver.dart | 22 +--------------------- 4 files changed, 49 insertions(+), 31 deletions(-) diff --git a/lib/src/ardrive_http.dart b/lib/src/ardrive_http.dart index c0fe788..e243959 100644 --- a/lib/src/ardrive_http.dart +++ b/lib/src/ardrive_http.dart @@ -4,11 +4,14 @@ import 'dart:io'; import 'dart:math'; import 'package:ardrive_http/src/responses.dart'; +import 'package:ardrive_http/src/utils.dart'; import 'package:cancellation_token_http/http.dart' as http_cancel; +import 'package:cancellation_token_http/retry.dart' as http_cancel_retry; import 'package:dio/dio.dart'; import 'package:dio_smart_retry/dio_smart_retry.dart'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; +import 'package:http/retry.dart' as http_retry; import 'package:isolated_worker/js_isolated_worker.dart'; import 'io/fetch_client_stub.dart' @@ -93,20 +96,31 @@ class ArDriveHTTP { } Future getAsByteStream(String url, {Completer? cancelWithReason}) async { - return kIsWeb - ? _getAsByteStreamWeb(url, cancelWithReason: cancelWithReason) - : _getAsByteStreamIO(url, cancelWithReason: cancelWithReason); + try { + return kIsWeb + ? _getAsByteStreamWeb(url, cancelWithReason: cancelWithReason) + : _getAsByteStreamIO(url, cancelWithReason: cancelWithReason); + } catch (error) { + throw ArDriveHTTPException( + retryAttempts: retryAttempts, + dioException: error, + ); + } } Future _getAsByteStreamWeb(String url, {Completer? cancelWithReason}) async { - final client = fetch.FetchClient(mode: fetch.RequestMode.cors); - - final response = await client.send( + final client = http_retry.RetryClient( + fetch.FetchClient(mode: fetch.RequestMode.cors), + when: (response) => retryStatusCodes.contains(response.statusCode), + onRetry: (_, __, ___) => retryAttempts++, + ); + + final response = (await client.send( http.Request( 'GET', Uri.parse(url), ), - ); + )) as fetch.FetchResponse; cancelWithReason?.future.then((value) { debugPrint('Cancelling request to $url with reason: $value'); @@ -123,7 +137,11 @@ class ArDriveHTTP { } Future _getAsByteStreamIO(String url, {Completer? cancelWithReason}) async { - final client = http_cancel.Client(); + final client = http_cancel_retry.RetryClient( + http_cancel.Client(), + when: (response) => retryStatusCodes.contains(response.statusCode), + onRetry: (_, __, ___) => retryAttempts++, + ); final cancellationToken = http_cancel.CancellationToken(); final response = await client.send( diff --git a/lib/src/utils.dart b/lib/src/utils.dart index 4598ab6..26659ab 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -5,3 +5,24 @@ void checkIsJsonAndAsBytesParams(isJson, asBytes) { ); } } + +List retryStatusCodes = [ + 408, + 429, + 440, + 460, + 499, + 500, + 502, + 503, + 504, + 520, + 521, + 522, + 523, + 524, + 525, + 527, + 598, + 599 +]; diff --git a/test/ardrive_http_test.dart b/test/ardrive_http_test.dart index 9e2700a..70f5d07 100644 --- a/test/ardrive_http_test.dart +++ b/test/ardrive_http_test.dart @@ -2,13 +2,12 @@ import 'dart:convert'; import 'dart:io'; import 'package:ardrive_http/ardrive_http.dart'; +import 'package:ardrive_http/src/utils.dart'; import 'package:async/async.dart'; import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; -import './webserver.dart'; - const String baseUrl = 'http://127.0.0.1:8080'; void main() { diff --git a/test/webserver.dart b/test/webserver.dart index 0f6a5cb..214730e 100644 --- a/test/webserver.dart +++ b/test/webserver.dart @@ -1,31 +1,11 @@ import 'dart:convert'; import 'dart:io'; +import 'package:ardrive_http/src/utils.dart'; import 'package:shelf/shelf.dart'; import 'package:shelf/shelf_io.dart' as shelf_io; import 'package:shelf_router/shelf_router.dart'; -List retryStatusCodes = [ - 408, - 429, - 440, - 460, - 499, - 500, - 502, - 503, - 504, - 520, - 521, - 522, - 523, - 524, - 525, - 527, - 598, - 599 -]; - const Map headers = { 'access-control-allow-origin': '*', 'access-control-allow-methods': 'GET, POST, OPTIONS', From fba9f3e6818dab0187cd75f68094253972175cba Mon Sep 17 00:00:00 2001 From: Elliot Sayes Date: Sun, 26 Mar 2023 21:06:20 +0800 Subject: [PATCH 11/11] Update flutter versions in other parts of project --- .fvm/fvm_config.json | 2 +- .github/workflows/pr.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.fvm/fvm_config.json b/.fvm/fvm_config.json index 6644814..1d78766 100644 --- a/.fvm/fvm_config.json +++ b/.fvm/fvm_config.json @@ -1,4 +1,4 @@ { - "flutterSdkVersion": "3.3.9", + "flutterSdkVersion": "3.7.0", "flavors": {} } diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 9d72c3d..dbe22ca 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -15,7 +15,7 @@ jobs: # Install Flutter - uses: subosito/flutter-action@v2 with: - flutter-version: '3.3.9' + flutter-version: '3.7.0' channel: 'stable' # Install Chrome