fix(trackers): cancel connect immediately

This commit is contained in:
edde746
2026-04-24 20:54:38 +02:00
parent 1de91a598c
commit df94e83592
7 changed files with 77 additions and 26 deletions
@@ -19,10 +19,11 @@ class AnilistAuthService {
Future<AnilistSession?> authorize({
required void Function(OAuthProxyStart) onCodeReady,
bool Function()? shouldCancel,
Future<void>? onCancel,
}) async {
final start = await _proxy.start('anilist');
onCodeReady(start);
final result = await _proxy.poll(start.session, shouldCancel: shouldCancel);
final result = await _proxy.poll(start.session, shouldCancel: shouldCancel, onCancel: onCancel);
if (result == null) return null;
return AnilistSession.fromProxyResult(result);
}
@@ -32,10 +32,19 @@ abstract class DeviceCodeAuthServiceBase<T> {
/// Drive the full flow. Invokes [onCodeReady] once with the code so the UI
/// can render the dialog, then polls until the user authorizes, denies, or
/// the code expires. Returns null on denied/expired/cancel.
Future<T?> authorize({required void Function(DeviceCode code) onCodeReady, bool Function()? shouldCancel}) async {
Future<T?> authorize({
required void Function(DeviceCode code) onCodeReady,
bool Function()? shouldCancel,
Future<void>? onCancel,
}) async {
final code = await createDeviceCode();
onCodeReady(code);
await for (final event in poller.pollDeviceCode(code, shouldCancel: shouldCancel, probe: () => probe(code))) {
await for (final event in poller.pollDeviceCode(
code,
shouldCancel: shouldCancel,
onCancel: onCancel,
probe: () => probe(code),
)) {
if (event is DevicePollSuccess) return buildSession(event.tokenResponse);
if (event is DevicePollDenied || event is DevicePollExpired) return null;
}
@@ -14,13 +14,18 @@ Stream<DevicePollEvent> pollDeviceCode(
DeviceCode code, {
required Future<DevicePollEvent> Function() probe,
bool Function()? shouldCancel,
Future<void>? onCancel,
}) async* {
var interval = Duration(seconds: code.interval);
final deadline = DateTime.now().add(Duration(seconds: code.expiresIn));
while (DateTime.now().isBefore(deadline)) {
if (shouldCancel != null && shouldCancel()) return;
await Future<void>.delayed(interval);
if (onCancel != null) {
await Future.any<void>([Future<void>.delayed(interval), onCancel]);
} else {
await Future<void>.delayed(interval);
}
if (shouldCancel != null && shouldCancel()) return;
final event = await probe();
@@ -34,10 +34,11 @@ class MalAuthService {
Future<MalSession?> authorize({
required void Function(OAuthProxyStart) onCodeReady,
bool Function()? shouldCancel,
Future<void>? onCancel,
}) async {
final start = await _proxy.start('mal');
onCodeReady(start);
final result = await _proxy.poll(start.session, shouldCancel: shouldCancel);
final result = await _proxy.poll(start.session, shouldCancel: shouldCancel, onCancel: onCancel);
if (result == null) return null;
return MalSession.fromProxyResult(result);
}
+14 -7
View File
@@ -48,18 +48,22 @@ class OAuthProxyClient {
/// Long-poll /auth/result?session=X until a completion event arrives.
///
/// Returns null if [shouldCancel] flips true before a result arrives. Throws
/// [OAuthProxyException] on unrecoverable errors (session gone, upstream
/// failure). The server holds each request for up to 50 s; 204 responses are
/// retried transparently.
Future<OAuthProxyResult?> poll(String session, {bool Function()? shouldCancel}) async {
/// Returns null if [shouldCancel] flips true between iterations or [onCancel]
/// completes mid-request. Throws [OAuthProxyException] on unrecoverable errors
/// (session gone, upstream failure). The server holds each request for up to
/// 50 s; 204 responses are retried transparently.
Future<OAuthProxyResult?> poll(String session, {bool Function()? shouldCancel, Future<void>? onCancel}) async {
final uri = Uri.parse('$baseUrl/auth/result').replace(queryParameters: {'session': session});
final cancelSentinel = Object();
// Subscribe to onCancel once; reusing this derived future avoids
// accumulating a fresh listener per loop iteration.
final cancelFuture = onCancel?.then((_) => cancelSentinel);
while (true) {
if (shouldCancel?.call() ?? false) return null;
final http.Response res;
final Object? raced;
try {
res = await _http.get(uri).timeout(const Duration(seconds: 65));
raced = await Future.any<Object?>([_http.get(uri).timeout(const Duration(seconds: 65)), ?cancelFuture]);
} on TimeoutException {
continue;
} catch (e) {
@@ -68,6 +72,9 @@ class OAuthProxyClient {
continue;
}
if (identical(raced, cancelSentinel)) return null;
final res = raced as http.Response;
if (res.statusCode == 204) continue; // server-side timeout, retry
if (res.statusCode == 410) {
throw const OAuthProxyException('Session expired or already used');