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 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 diff --git a/lib/src/ardrive_http.dart b/lib/src/ardrive_http.dart index 280f04e..e243959 100644 --- a/lib/src/ardrive_http.dart +++ b/lib/src/ardrive_http.dart @@ -4,11 +4,19 @@ 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' + if (dart.library.html) 'package:fetch_client/fetch_client.dart' as fetch; + const List jsScriptsToImport = ['ardrive-http.js']; String normalizeResponseTypeToJS(ResponseType responseType) { @@ -87,6 +95,77 @@ class ArDriveHTTP { return get(url: url, responseType: ResponseType.bytes); } + Future getAsByteStream(String url, {Completer? cancelWithReason}) async { + 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 = 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'); + 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 { + 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( + http_cancel.Request( + 'GET', + Uri.parse(url), + ), + cancellationToken: cancellationToken, + ); + + 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, + 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/lib/src/io/fetch_client_stub.dart b/lib/src/io/fetch_client_stub.dart new file mode 100644 index 0000000..c2f1acb --- /dev/null +++ b/lib/src/io/fetch_client_stub.dart @@ -0,0 +1,90 @@ +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 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({ + 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(); + } +} 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/pubspec.yaml b/pubspec.yaml index 374c1c2..7901f76 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: @@ -21,11 +21,14 @@ 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 + fetch_client: ^1.0.0 flutter: sdk: flutter + http: ^0.13.5 isolated_worker: ^0.1.1 shelf: ^1.4.0 shelf_router: ^1.1.3 @@ -34,3 +37,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..70f5d07 100644 --- a/test/ardrive_http_test.dart +++ b/test/ardrive_http_test.dart @@ -2,12 +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() { @@ -62,6 +62,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'; 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',