Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for download via byte stream #20

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
2 changes: 1 addition & 1 deletion .fvm/fvm_config.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"flutterSdkVersion": "3.3.9",
"flutterSdkVersion": "3.7.0",
"flavors": {}
}
2 changes: 1 addition & 1 deletion .github/workflows/pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
79 changes: 79 additions & 0 deletions lib/src/ardrive_http.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> jsScriptsToImport = <String>['ardrive-http.js'];

String normalizeResponseTypeToJS(ResponseType responseType) {
Expand Down Expand Up @@ -87,6 +95,77 @@ class ArDriveHTTP {
return get(url: url, responseType: ResponseType.bytes);
}

Future<ArDriveHTTPResponse> getAsByteStream(String url, {Completer<String>? cancelWithReason}) async {
try {
return kIsWeb
? _getAsByteStreamWeb(url, cancelWithReason: cancelWithReason)
: _getAsByteStreamIO(url, cancelWithReason: cancelWithReason);
} catch (error) {
throw ArDriveHTTPException(
retryAttempts: retryAttempts,
dioException: error,
);
}
}

Future<ArDriveHTTPResponse> _getAsByteStreamWeb(String url, {Completer<String>? 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(
elliotsayes marked this conversation as resolved.
Show resolved Hide resolved
'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<ArDriveHTTPResponse> _getAsByteStreamIO(String url, {Completer<String>? 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<ArDriveHTTPResponse> _getIO(Map params) async {
final String url = params['url'];
final ResponseType responseType = params['responseType'];
Expand Down
90 changes: 90 additions & 0 deletions lib/src/io/fetch_client_stub.dart
Original file line number Diff line number Diff line change
@@ -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<FetchResponse> send(BaseRequest request) {
throw UnimplementedError();
}

@override
void close() {
throw UnimplementedError();
}

@override
Future<Response> delete(Uri url, {Map<String, String>? headers, Object? body, Encoding? encoding}) {
throw UnimplementedError();
}

@override
Future<Response> get(Uri url, {Map<String, String>? headers}) {
throw UnimplementedError();
}

@override
Future<Response> head(Uri url, {Map<String, String>? headers}) {
throw UnimplementedError();
}

@override
Future<Response> patch(Uri url, {Map<String, String>? headers, Object? body, Encoding? encoding}) {
throw UnimplementedError();
}

@override
Future<Response> post(Uri url, {Map<String, String>? headers, Object? body, Encoding? encoding}) {
throw UnimplementedError();
}

@override
Future<Response> put(Uri url, {Map<String, String>? headers, Object? body, Encoding? encoding}) {
throw UnimplementedError();
}

@override
Future<String> read(Uri url, {Map<String, String>? headers}) {
throw UnimplementedError();
}

@override
Future<Uint8List> readBytes(Uri url, {Map<String, String>? headers}) {
throw UnimplementedError();
}
}
21 changes: 21 additions & 0 deletions lib/src/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,24 @@ void checkIsJsonAndAsBytesParams(isJson, asBytes) {
);
}
}

List<int> retryStatusCodes = [
408,
429,
440,
460,
499,
500,
502,
503,
504,
520,
521,
522,
523,
524,
525,
527,
598,
599
];
6 changes: 5 additions & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ publish_to: none

environment:
sdk: '>=2.18.5 <3.0.0'
flutter: 3.3.9
flutter: 3.7.0
elliotsayes marked this conversation as resolved.
Show resolved Hide resolved
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO for @matibat: check what are the downsides of bumping the version for localizations

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just FYR already fixed a bunch of localisation errors in the ArDrive-web PR


script_runner:
shell:
Expand All @@ -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
Expand All @@ -34,3 +37,4 @@ dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.1
async: ^2.9.0
14 changes: 12 additions & 2 deletions test/ardrive_http_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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<Uint8List>;

expect(collectBytes(byteStream), completion(Uint8List.fromList([111, 107])));
expect(getResponse.retryAttempts, 0);
});

test('fail without retry', () async {
const String url = '$baseUrl/404';

Expand Down
22 changes: 1 addition & 21 deletions test/webserver.dart
Original file line number Diff line number Diff line change
@@ -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<int> retryStatusCodes = [
408,
429,
440,
460,
499,
500,
502,
503,
504,
520,
521,
522,
523,
524,
525,
527,
598,
599
];

const Map<String, Object> headers = {
'access-control-allow-origin': '*',
'access-control-allow-methods': 'GET, POST, OPTIONS',
Expand Down