Skip to content

Commit

Permalink
feat: make cancellation more stable
Browse files Browse the repository at this point in the history
  • Loading branch information
Tienisto committed Nov 4, 2024
1 parent 8c458df commit 76fbe21
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 10 deletions.
112 changes: 106 additions & 6 deletions rhttp/example/lib/cancellation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import 'package:flutter/material.dart';
import 'package:rhttp/rhttp.dart';
import 'package:rhttp_example/widgets/response_card.dart';

Future<void> main() async {
await Rhttp.init();
Expand Down Expand Up @@ -43,8 +44,7 @@ class _MyAppState extends State<MyApp> {
);
final resFuture = client!.requestBytes(
method: HttpMethod.get,
url:
'https://github.com/localsend/localsend/releases/download/v1.15.3/LocalSend-1.15.3-linux-x86-64.AppImage',
url: 'https://github.com/localsend/localsend/releases/download/v1.15.3/LocalSend-1.15.3-linux-x86-64.AppImage',
cancelToken: cancelToken,
);

Expand All @@ -62,11 +62,111 @@ class _MyAppState extends State<MyApp> {
print(e);
}
},
child: const Text('Test'),
child: const Text('Cancel after 1 second'),
),
if (response != null) Text(response!.version.toString()),
if (response != null) Text(response!.statusCode.toString()),
if (response != null) Text(response!.headers.toString()),
ElevatedButton(
onPressed: () async {
try {
final cancelToken = CancelToken();
client ??= await RhttpClient.create(
settings: const ClientSettings(
timeoutSettings: TimeoutSettings(
timeout: Duration(seconds: 10),
),
),
);
final resFuture = client!.requestBytes(
method: HttpMethod.get,
url: 'https://github.com/localsend/localsend/releases/download/v1.15.3/LocalSend-1.15.3-linux-x86-64.AppImage',
cancelToken: cancelToken,
);

await cancelToken.cancel();

final res = await resFuture;

setState(() {
response = res;
});
} catch (e) {
print(e);
}
},
child: const Text('Cancel immediately'),
),
ElevatedButton(
onPressed: () async {
try {
final cancelToken = CancelToken();
client ??= await RhttpClient.create(
settings: const ClientSettings(
timeoutSettings: TimeoutSettings(
timeout: Duration(seconds: 10),
),
),
);
final resFuture = client!.requestBytes(
method: HttpMethod.get,
url: 'https://github.com/localsend/localsend/releases/download/v1.15.3/LocalSend-1.15.3-linux-x86-64.AppImage',
cancelToken: cancelToken,
);

await cancelToken.cancel();
await cancelToken.cancel();

final res = await resFuture;

setState(() {
response = res;
});
} catch (e) {
print(e);
}
},
child: const Text('Cancel multiple times'),
),
ElevatedButton(
onPressed: () async {
final cancelToken = CancelToken();
client ??= await RhttpClient.create(
settings: const ClientSettings(
timeoutSettings: TimeoutSettings(
timeout: Duration(seconds: 10),
),
),
);

final resFuture = client!.requestBytes(
method: HttpMethod.get,
url: 'https://github.com/localsend/localsend/releases/download/v1.15.3/LocalSend-1.15.3-linux-x86-64.AppImage',
cancelToken: cancelToken,
);

final resFuture2 = client!.requestBytes(
method: HttpMethod.get,
url: 'https://github.com/localsend/localsend/releases/download/v1.16.0/LocalSend-1.16.0-linux-x86-64.AppImage',
cancelToken: cancelToken,
);

Future.delayed(const Duration(seconds: 1), () async {
await cancelToken.cancel();
});

try {
await resFuture;
} catch (e) {
print(e);
}

try {
await resFuture2;
} catch (e) {
print(e);
}
},
child: const Text('Cancel multiple requests'),
),
if (response != null) ResponseCard(response: response!),
],
),
),
Expand Down
2 changes: 1 addition & 1 deletion rhttp/example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ packages:
path: ".."
relative: true
source: path
version: "0.8.1"
version: "0.9.1"
riverpod:
dependency: transitive
description:
Expand Down
24 changes: 21 additions & 3 deletions rhttp/lib/src/model/cancel_token.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,26 @@ import 'package:rhttp/src/rust/lib.dart' as rust_lib;
/// If a cancelled token is passed to a request method,
/// the request is cancelled immediately.
class CancelToken {
final _refController =
StreamController<rust_lib.CancellationToken>.broadcast();
final _refController = StreamController<rust_lib.CancellationToken>();
final _firstRef = Completer<rust_lib.CancellationToken>();
final _refs = <rust_lib.CancellationToken>[];

bool _isCancelled = false;

/// Whether the cancellation process has started.
/// This is different from [isCancelled] because otherwise,
/// we would not receive the stream event.
bool _lock = false;

/// Whether the request has been cancelled.
bool get isCancelled => _isCancelled;

CancelToken() {
_refController.stream.listen((ref) {
_refs.add(ref);
if (_refs.length == 1) {
_firstRef.complete(ref);
}
});
}

Expand All @@ -41,13 +49,23 @@ class CancelToken {
/// If the [CancelToken] is not passed to the request method,
/// this method never finishes.
Future<void> cancel() async {
if (_isCancelled) {
return;
}

if (_lock) {
return;
}

_lock = true;

if (_refs.isNotEmpty) {
for (final ref in _refs) {
await rust.cancelRequest(token: ref);
}
} else {
// We need to wait for the ref to be set.
final ref = await _refController.stream.first;
final ref = await _firstRef.future;
await rust.cancelRequest(token: ref);
}

Expand Down

0 comments on commit 76fbe21

Please sign in to comment.