Skip to content

Commit

Permalink
⚡️ Split web_adapter (#2223)
Browse files Browse the repository at this point in the history
Breaks the web business into a separate package to prepare for further changes regarding the WASM builds.

### Additional context and info (if any)

Here we put a v1 for the package. Once it works, we will migrate to the
js_interop and `package:web` implementation and publish v2.

---------

Co-authored-by: hhh <[email protected]>
  • Loading branch information
AlexV525 and huanghui1998hhh authored Jun 15, 2024
1 parent b87a3b0 commit 4f4cdee
Show file tree
Hide file tree
Showing 77 changed files with 1,248 additions and 455 deletions.
1 change: 1 addition & 0 deletions .github/ISSUE_TEMPLATE/bug.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ body:
- cookie_manager
- http2_adapter
- native_dio_adapter
- web_adapter
validations:
required: true
- type: input
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ jobs:
cache: true
flutter-version: ${{ matrix.sdk == 'min' && '3.3.0' || '' }}
channel: ${{ matrix.sdk == 'min' && '' || matrix.channel }}
- run: dart pub get
- name: Prepare dependencies for the project management
run: dart pub get
- uses: bluefireteam/melos-action@v3
with:
run-bootstrap: false
Expand Down
3 changes: 2 additions & 1 deletion .idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion README-ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,12 @@ Language: [English](README.md) | 简体中文
[![Pub](https://img.shields.io/pub/v/dio_http2_adapter.svg?label=dev&include_prereleases)](https://pub.flutter-io.cn/packages/dio_http2_adapter)
- native_dio_adapter: [链接](plugins/native_dio_adapter)
[![Pub](https://img.shields.io/pub/v/native_dio_adapter.svg?label=dev&include_prereleases)](https://pub.dev/packages/native_dio_adapter)
- web_adapter: [链接](plugins/web_adapter)
[![Pub](https://img.shields.io/pub/v/dio_web_adapter.svg?label=dev&include_prereleases)](https://pub.dev/packages/dio_web_adapter)

### 示例

- example: [链接](example)
- example: [链接](example_dart)
- example_flutter_app: [链接](example_flutter_app)

## 版权 & 协议
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,12 @@ To know about our compatibility policy, see the [Compatibility Policy][] doc.
[![Pub](https://img.shields.io/pub/v/dio_http2_adapter.svg?label=dev&include_prereleases)](https://pub.dev/packages/dio_http2_adapter)
- native_dio_adapter: [link](plugins/native_dio_adapter)
[![Pub](https://img.shields.io/pub/v/native_dio_adapter.svg?label=dev&include_prereleases)](https://pub.dev/packages/native_dio_adapter)
- web_adapter: [link](plugins/web_adapter)
[![Pub](https://img.shields.io/pub/v/dio_web_adapter.svg?label=dev&include_prereleases)](https://pub.dev/packages/dio_web_adapter)

### Examples

- example: [link](example)
- example: [link](example_dart)
- example_flutter_app: [link](example_flutter_app)

## Copyright & License
Expand Down
1 change: 1 addition & 0 deletions dio/lib/src/adapter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'dart:typed_data';
import 'package:meta/meta.dart';

import 'adapters/io_adapter.dart'
if (dart.library.js_interop) 'adapters/browser_adapter.dart'
if (dart.library.html) 'adapters/browser_adapter.dart' as adapter;
import 'headers.dart';
import 'options.dart';
Expand Down
315 changes: 2 additions & 313 deletions dio/lib/src/adapters/browser_adapter.dart
Original file line number Diff line number Diff line change
@@ -1,313 +1,2 @@
import 'dart:async';
import 'dart:convert';
import 'dart:html';
import 'dart:typed_data';

import 'package:meta/meta.dart';

import '../adapter.dart';
import '../dio_exception.dart';
import '../headers.dart';
import '../options.dart';
import '../utils.dart';

HttpClientAdapter createAdapter() => BrowserHttpClientAdapter();

/// The default [HttpClientAdapter] for Web platforms.
class BrowserHttpClientAdapter implements HttpClientAdapter {
BrowserHttpClientAdapter({this.withCredentials = false});

/// These are aborted if the client is closed.
@visibleForTesting
final xhrs = <HttpRequest>{};

/// Whether to send credentials such as cookies or authorization headers for
/// cross-site requests.
///
/// Defaults to `false`.
///
/// You can also override this value using `Options.extra['withCredentials']`
/// for each request.
bool withCredentials;

@override
Future<ResponseBody> fetch(
RequestOptions options,
Stream<Uint8List>? requestStream,
Future<void>? cancelFuture,
) async {
final xhr = HttpRequest();
xhrs.add(xhr);
xhr
..open(options.method, '${options.uri}')
..responseType = 'arraybuffer';

final withCredentialsOption = options.extra['withCredentials'];
if (withCredentialsOption != null) {
xhr.withCredentials = withCredentialsOption == true;
} else {
xhr.withCredentials = withCredentials;
}

options.headers.remove(Headers.contentLengthHeader);
options.headers.forEach((key, v) {
if (v is Iterable) {
xhr.setRequestHeader(key, v.join(', '));
} else {
xhr.setRequestHeader(key, v.toString());
}
});

final sendTimeout = options.sendTimeout ?? Duration.zero;
final connectTimeout = options.connectTimeout ?? Duration.zero;
final receiveTimeout = options.receiveTimeout ?? Duration.zero;
final xhrTimeout = (connectTimeout + receiveTimeout).inMilliseconds;
xhr.timeout = xhrTimeout;

final completer = Completer<ResponseBody>();

xhr.onLoad.first.then((_) {
final Uint8List body = (xhr.response as ByteBuffer).asUint8List();
completer.complete(
ResponseBody.fromBytes(
body,
xhr.status!,
headers: xhr.responseHeaders.map((k, v) => MapEntry(k, v.split(','))),
statusMessage: xhr.statusText,
isRedirect: xhr.status == 302 ||
xhr.status == 301 ||
options.uri.toString() != xhr.responseUrl,
),
);
});

Timer? connectTimeoutTimer;
if (connectTimeout > Duration.zero) {
connectTimeoutTimer = Timer(
connectTimeout,
() {
connectTimeoutTimer = null;
if (completer.isCompleted) {
// connectTimeout is triggered after the fetch has been completed.
return;
}
xhr.abort();
completer.completeError(
DioException.connectionTimeout(
requestOptions: options,
timeout: connectTimeout,
),
StackTrace.current,
);
},
);
}

// This code is structured to call `xhr.upload.onProgress.listen` only when
// absolutely necessary, because registering an xhr upload listener prevents
// the request from being classified as a "simple request" by the CORS spec.
// Reference: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests
// Upload progress events only get triggered if the request body exists,
// so we can check it beforehand.
if (requestStream != null) {
if (connectTimeoutTimer != null) {
xhr.upload.onProgress.listen((event) {
connectTimeoutTimer?.cancel();
connectTimeoutTimer = null;
});
}

if (sendTimeout > Duration.zero) {
final uploadStopwatch = Stopwatch();
xhr.upload.onProgress.listen((event) {
if (!uploadStopwatch.isRunning) {
uploadStopwatch.start();
}
final duration = uploadStopwatch.elapsed;
if (duration > sendTimeout) {
uploadStopwatch.stop();
completer.completeError(
DioException.sendTimeout(
timeout: sendTimeout,
requestOptions: options,
),
StackTrace.current,
);
xhr.abort();
}
});
}

final onSendProgress = options.onSendProgress;
if (onSendProgress != null) {
xhr.upload.onProgress.listen((event) {
if (event.loaded != null && event.total != null) {
onSendProgress(event.loaded!, event.total!);
}
});
}
} else {
if (sendTimeout > Duration.zero) {
debugLog(
'sendTimeout cannot be used without a request body to send',
StackTrace.current,
);
}
if (options.onSendProgress != null) {
debugLog(
'onSendProgress cannot be used without a request body to send',
StackTrace.current,
);
}
}

final receiveStopwatch = Stopwatch();
Timer? receiveTimer;

void stopWatchReceiveTimeout() {
receiveTimer?.cancel();
receiveTimer = null;
receiveStopwatch.stop();
}

void watchReceiveTimeout() {
if (receiveTimeout <= Duration.zero) {
return;
}
receiveStopwatch.reset();
if (!receiveStopwatch.isRunning) {
receiveStopwatch.start();
}
receiveTimer?.cancel();
receiveTimer = Timer(receiveTimeout, () {
if (!completer.isCompleted) {
xhr.abort();
completer.completeError(
DioException.receiveTimeout(
timeout: receiveTimeout,
requestOptions: options,
),
StackTrace.current,
);
}
stopWatchReceiveTimeout();
});
}

xhr.onProgress.listen(
(ProgressEvent event) {
if (connectTimeoutTimer != null) {
connectTimeoutTimer!.cancel();
connectTimeoutTimer = null;
}
watchReceiveTimeout();
if (options.onReceiveProgress != null &&
event.loaded != null &&
event.total != null) {
options.onReceiveProgress!(event.loaded!, event.total!);
}
},
onDone: () => stopWatchReceiveTimeout(),
);

xhr.onError.first.then((_) {
connectTimeoutTimer?.cancel();
// Unfortunately, the underlying XMLHttpRequest API doesn't expose any
// specific information about the error itself.
// See also: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequestEventTarget/onerror
completer.completeError(
DioException.connectionError(
requestOptions: options,
reason: 'The XMLHttpRequest onError callback was called. '
'This typically indicates an error on the network layer.',
),
StackTrace.current,
);
});

xhr.onTimeout.first.then((_) {
final isConnectTimeout = connectTimeoutTimer != null;
if (connectTimeoutTimer != null) {
connectTimeoutTimer?.cancel();
}
if (!completer.isCompleted) {
if (isConnectTimeout) {
completer.completeError(
DioException.connectionTimeout(
timeout: connectTimeout,
requestOptions: options,
),
);
} else {
completer.completeError(
DioException.receiveTimeout(
timeout: Duration(milliseconds: xhrTimeout),
requestOptions: options,
),
StackTrace.current,
);
}
}
});

cancelFuture?.then((_) {
if (xhr.readyState < HttpRequest.DONE &&
xhr.readyState > HttpRequest.UNSENT) {
connectTimeoutTimer?.cancel();
try {
xhr.abort();
} catch (_) {}
if (!completer.isCompleted) {
completer.completeError(
DioException.requestCancelled(
requestOptions: options,
reason: 'The XMLHttpRequest was aborted.',
),
);
}
}
});

if (requestStream != null) {
if (options.method == 'GET') {
debugLog(
'GET request with a body data are not support on the '
'web platform. Use POST/PUT instead.',
StackTrace.current,
);
}
final completer = Completer<Uint8List>();
final sink = ByteConversionSink.withCallback(
(bytes) => completer.complete(
bytes is Uint8List ? bytes : Uint8List.fromList(bytes),
),
);
requestStream.listen(
sink.add,
onError: (Object e, StackTrace s) => completer.completeError(e, s),
onDone: sink.close,
cancelOnError: true,
);
final bytes = await completer.future;
xhr.send(bytes);
} else {
xhr.send();
}
return completer.future.whenComplete(() {
xhrs.remove(xhr);
});
}

/// Closes the client.
///
/// This terminates all active requests.
@override
void close({bool force = false}) {
if (force) {
for (final xhr in xhrs) {
xhr.abort();
}
}
xhrs.clear();
}
}
export 'package:dio_web_adapter/dio_web_adapter.dart'
show createAdapter, BrowserHttpClientAdapter;
4 changes: 3 additions & 1 deletion dio/lib/src/compute/compute.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@

import 'dart:async';

import 'compute_io.dart' if (dart.library.html) 'compute_web.dart' as _c;
import 'compute_io.dart'
if (dart.library.js_interop) 'compute_web.dart'
if (dart.library.html) 'compute_web.dart' as _c;

/// Signature for the callback passed to [compute].
///
Expand Down
Loading

0 comments on commit 4f4cdee

Please sign in to comment.