relatica/lib/riverpod_controllers/networking/network_services.dart

294 wiersze
8.5 KiB
Dart

import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart' as http;
import 'package:http/http.dart';
import 'package:logging/logging.dart';
import 'package:result_monad/result_monad.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../../globals.dart';
import '../../models/exec_error.dart';
import '../../models/networking/paged_response.dart';
import '../globals_services.dart';
import '../rp_provider_extension.dart';
import '../settings_services.dart';
part 'network_services.g.dart';
http.Response requestTimeout() => http.Response('Client side timeout', 408);
class NetworkRequest {
final Uri url;
final Map<String, dynamic>? body;
final Map<String, String> headers;
final Duration? timeout;
NetworkRequest(
this.url, {
this.body,
this.headers = const {},
this.timeout,
});
@override
String toString() {
return 'NetworkRequest{url: $url, body: $body, headers: $headers, timeout: $timeout}';
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is NetworkRequest &&
runtimeType == other.runtimeType &&
url == other.url &&
mapEquals(body, other.body) &&
mapEquals(headers, other.headers) &&
timeout == other.timeout;
@override
int get hashCode =>
url.hashCode ^
Object.hashAll(body?.entries.toList() ?? []) ^
Object.hashAll(headers.entries.toList()) ^
timeout.hashCode;
}
final _logger = Logger('NetworkServicesProviders');
extension _DebounceAndCancelExtension on Ref {
/// Wait for [duration] (defaults to 500ms), and then return a [http.Client]
/// which can be used to make a request.
///
/// That client will automatically be closed when the provider is disposed.
Future<http.Client> getDebouncedHttpClient([Duration? duration]) async {
// First, we handle debouncing.
var didDispose = false;
onDispose(() => didDispose = true);
// We delay multiple requests in a row within a specified interval or 0.5 s
await Future<void>.delayed(duration ?? const Duration(milliseconds: 500));
// If the provider was disposed during the delay, it means that the user
// refreshed again. We throw an exception to cancel the request.
// It is safe to use an exception here, as it will be caught by Riverpod.
if (didDispose) {
throw Exception('Cancelled');
}
// We now create the client and close it when the provider is disposed.
final client = http.Client();
onDispose(client.close);
// Finally, we return the client to allow our provider to make the request.
return client;
}
}
@riverpod
Map<String, String> userAgentHeader(Ref ref) {
final userAgent = ref.watch(userAgentProvider);
return {
'user-agent': userAgent,
};
}
@riverpod
Future<Result<Uint8List, ExecError>> httpGetRawBytes(
Ref ref,
NetworkRequest request,
) async {
_logger.fine('GET Raw Bytes: ${request.url}');
//TODO determine if this timeout needs to be the timeout for the call or how long it sticks around for after it is first executed
final timeoutSetting = ref.read(friendicaApiTimeoutSettingProvider);
final timeout = request.timeout ?? timeoutSetting;
ref.cacheFor(timeout);
final requestHeaders = Map<String, String>.from(request.headers);
if (usePhpDebugging) {
requestHeaders['Cookie'] = 'XDEBUG_SESSION=PHPSTORM;path=/';
}
try {
final client = await ref.getDebouncedHttpClient();
final httpRequest = client.get(
request.url,
headers: requestHeaders,
);
final response = await httpRequest.timeout(
timeout,
onTimeout: requestTimeout,
);
if (response.statusCode != 200) {
return Result.error(_buildErrorFromResponse(response));
}
return Result.ok(response.bodyBytes);
} catch (e) {
return Result.error(
ExecError(type: ErrorType.localError, message: e.toString()));
}
}
@riverpod
Future<Result<PagedResponse<String>, ExecError>> httpGet(
Ref ref,
NetworkRequest request,
) async {
_logger.fine('GET: ${request.url}');
//TODO determine if this timeout needs to be the timeout for the call or how long it sticks around for after it is first executed
final timeoutSetting = ref.read(friendicaApiTimeoutSettingProvider);
final timeout = request.timeout ?? timeoutSetting;
ref.cacheFor(timeout);
final requestHeaders = Map<String, String>.from(request.headers);
if (usePhpDebugging) {
requestHeaders['Cookie'] = 'XDEBUG_SESSION=PHPSTORM;path=/';
}
try {
final client = await ref.getDebouncedHttpClient();
final httpRequest = client.get(
request.url,
headers: requestHeaders,
);
final response = await httpRequest.timeout(
timeout,
onTimeout: requestTimeout,
);
if (response.statusCode != 200) {
return Result.error(_buildErrorFromResponse(response));
}
return PagedResponse.fromLinkHeader(
response.headers['link'],
utf8.decode(response.bodyBytes),
);
} catch (e) {
return Result.error(
ExecError(type: ErrorType.localError, message: e.toString()));
}
}
@riverpod
Future<Result<String, ExecError>> httpPost(
Ref ref,
NetworkRequest request,
) async {
_logger.fine('POST: ${request.url} \n Body: ${request.body}');
final timeoutSetting = ref.read(friendicaApiTimeoutSettingProvider);
final timeout = request.timeout ?? timeoutSetting;
ref.cacheFor(timeout);
final requestHeaders = Map<String, String>.from(request.headers);
if (usePhpDebugging) {
requestHeaders['Cookie'] = 'XDEBUG_SESSION=PHPSTORM;path=/';
}
try {
final client = await ref.getDebouncedHttpClient();
final httpRequest = client.post(
request.url,
headers: requestHeaders,
body: jsonEncode(request.body),
);
final response = await httpRequest.timeout(
timeout,
onTimeout: requestTimeout,
);
if (response.statusCode != 200) {
return Result.error(_buildErrorFromResponse(response));
}
return Result.ok(utf8.decode(response.bodyBytes));
} catch (e) {
return Result.error(
ExecError(type: ErrorType.localError, message: e.toString()));
}
}
@riverpod
Future<Result<String, ExecError>> httpPut(
Ref ref,
NetworkRequest request,
) async {
_logger.fine('PUT: ${request.url} \n Body: ${request.body}');
final timeoutSetting = ref.read(friendicaApiTimeoutSettingProvider);
final timeout = request.timeout ?? timeoutSetting;
ref.cacheFor(timeout);
final requestHeaders = Map<String, String>.from(request.headers);
if (usePhpDebugging) {
requestHeaders['Cookie'] = 'XDEBUG_SESSION=PHPSTORM;path=/';
}
try {
final client = await ref.getDebouncedHttpClient();
final httpRequest = client.put(
request.url,
headers: requestHeaders,
body: jsonEncode(request.body),
);
final response = await httpRequest.timeout(
timeout,
onTimeout: requestTimeout,
);
if (response.statusCode != 200) {
return Result.error(_buildErrorFromResponse(response));
}
return Result.ok(utf8.decode(response.bodyBytes));
} catch (e) {
return Result.error(
ExecError(type: ErrorType.localError, message: e.toString()));
}
}
@riverpod
Future<Result<String, ExecError>> httpDelete(
Ref ref,
NetworkRequest request,
) async {
_logger.fine('DELETE: ${request.url}');
final timeoutSetting = ref.read(friendicaApiTimeoutSettingProvider);
final timeout = request.timeout ?? timeoutSetting;
ref.cacheFor(timeout);
final requestHeaders = Map<String, String>.from(request.headers);
if (usePhpDebugging) {
requestHeaders['Cookie'] = 'XDEBUG_SESSION=PHPSTORM;path=/';
}
try {
final client = await ref.getDebouncedHttpClient();
final httpRequest = client.delete(
request.url,
headers: requestHeaders,
body: jsonEncode(request.body),
);
final response = await httpRequest.timeout(
timeout,
onTimeout: requestTimeout,
);
if (response.statusCode != 200) {
return Result.error(_buildErrorFromResponse(response));
}
return Result.ok(utf8.decode(response.bodyBytes));
} catch (e) {
return Result.error(
ExecError(type: ErrorType.localError, message: e.toString()));
}
}
ExecError _buildErrorFromResponse(Response response) {
final type = switch (response.statusCode) {
401 => ErrorType.authentication,
404 => ErrorType.notFound,
408 => ErrorType.timeoutError,
_ => ErrorType.serverError,
};
final msg = '${response.statusCode}: ${response.reasonPhrase}';
return ExecError(type: type, message: msg);
}