Skip to content

Commit

Permalink
♻️ Move all source Dart files to *_impl.dart to avoid naming collis…
Browse files Browse the repository at this point in the history
…ion (#2383)

Workaround with dart-lang/sdk#56498

### Additional context and info (if any)

v1 also needs to be updated.
  • Loading branch information
AlexV525 authored Feb 25, 2025
1 parent 0ddb355 commit 23d218c
Show file tree
Hide file tree
Showing 11 changed files with 458 additions and 437 deletions.
3 changes: 2 additions & 1 deletion plugins/web_adapter/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

## Unreleased

*None.*
- Move all source Dart files to `*_impl.dart` to avoid naming collision.
This is a workaround of https://github.com/dart-lang/sdk/issues/56498.

## 2.1.0

Expand Down
8 changes: 4 additions & 4 deletions plugins/web_adapter/lib/dio_web_adapter.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
library dio_web_adapter;

export 'src/adapter.dart';
export 'src/compute.dart';
export 'src/adapter_impl.dart';
export 'src/compute_impl.dart';
export 'src/dio_impl.dart';
export 'src/multipart_file.dart';
export 'src/progress_stream.dart';
export 'src/multipart_file_impl.dart';
export 'src/progress_stream_impl.dart';
340 changes: 5 additions & 335 deletions plugins/web_adapter/lib/src/adapter.dart
Original file line number Diff line number Diff line change
@@ -1,335 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'dart:js_interop';
import 'dart:typed_data';

import 'package:dio/dio.dart';
import 'package:dio/src/utils.dart';
import 'package:meta/meta.dart';
import 'package:web/web.dart' as web;

BrowserHttpClientAdapter 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 = <web.XMLHttpRequest>{};

/// 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 = web.XMLHttpRequest();
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 onSendProgress = options.onSendProgress;
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 ByteBuffer body = (xhr.response as JSArrayBuffer).toDart;
completer.complete(
ResponseBody.fromBytes(
body.asUint8List(),
xhr.status,
headers: xhr.getResponseHeaders(),
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) {
final xhrUploadProgressStream =
web.EventStreamProviders.progressEvent.forTarget(xhr.upload);

if (connectTimeoutTimer != null) {
xhrUploadProgressStream.listen((_) {
connectTimeoutTimer?.cancel();
connectTimeoutTimer = null;
});
}

if (sendTimeout > Duration.zero) {
final uploadStopwatch = Stopwatch();
xhrUploadProgressStream.listen((_) {
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();
}
});
}

if (onSendProgress != null) {
xhrUploadProgressStream.listen((event) {
onSendProgress(event.loaded, event.total);
});
}
} else {
if (sendTimeout > Duration.zero) {
warningLog(
'sendTimeout cannot be used without a request body to send on Web',
StackTrace.current,
);
}
if (onSendProgress != null) {
warningLog(
'onSendProgress cannot be used without a request body to send on Web',
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(
(event) {
if (connectTimeoutTimer != null) {
connectTimeoutTimer!.cancel();
connectTimeoutTimer = null;
}
watchReceiveTimeout();
if (options.onReceiveProgress != 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,
);
});

web.EventStreamProviders.timeoutEvent.forTarget(xhr).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 < web.XMLHttpRequest.DONE &&
xhr.readyState > web.XMLHttpRequest.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') {
warningLog(
'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.toJS);
} 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();
}
}

extension on web.XMLHttpRequest {
Map<String, List<String>> getResponseHeaders() {
final headersString = getAllResponseHeaders();
final headers = <String, List<String>>{};
if (headersString.isEmpty) {
return headers;
}
final headersList = headersString.split('\r\n');
for (final header in headersList) {
if (header.isEmpty) {
continue;
}

final splitIdx = header.indexOf(': ');
if (splitIdx == -1) {
continue;
}
final key = header.substring(0, splitIdx).toLowerCase();
final value = header.substring(splitIdx + 2);
(headers[key] ??= []).add(value);
}
return headers;
}
}
@Deprecated(
'Import adapter_impl.dart instead. '
'See https://github.com/dart-lang/sdk/issues/56498',
)
export 'adapter_impl.dart';
Loading

0 comments on commit 23d218c

Please sign in to comment.