initial commit
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import '../connections/connection_providers.dart';
|
||||
import 'node_master_api_client.dart';
|
||||
|
||||
part 'api_client_provider.g.dart';
|
||||
|
||||
/// Rebuilt (not mutated) whenever the active connection changes, so every
|
||||
/// watcher automatically rebuilds against the new host with no manual
|
||||
/// invalidation elsewhere. Returns null when no connection is configured —
|
||||
/// `app_router.dart` redirects to Settings in that case rather than every
|
||||
/// screen needing its own "no connection" branch.
|
||||
@riverpod
|
||||
NodeMasterApiClient? apiClient(Ref ref) {
|
||||
final connection = ref.watch(activeConnectionProvider);
|
||||
if (connection == null) return null;
|
||||
|
||||
final cancelToken = CancelToken();
|
||||
ref.onDispose(() => cancelToken.cancel('Active connection changed.'));
|
||||
|
||||
final baseUrl = connection.baseUrl.endsWith('/')
|
||||
? connection.baseUrl.substring(0, connection.baseUrl.length - 1)
|
||||
: connection.baseUrl;
|
||||
|
||||
final dio = Dio(
|
||||
BaseOptions(
|
||||
baseUrl: '$baseUrl/v1',
|
||||
connectTimeout: const Duration(seconds: 8),
|
||||
sendTimeout: const Duration(seconds: 8),
|
||||
receiveTimeout: const Duration(seconds: 15),
|
||||
contentType: 'application/json',
|
||||
),
|
||||
);
|
||||
if (kDebugMode) {
|
||||
dio.interceptors.add(LogInterceptor(requestBody: true, responseBody: true));
|
||||
}
|
||||
|
||||
return NodeMasterApiClient(dio, cancelToken);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'api_client_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
/// Rebuilt (not mutated) whenever the active connection changes, so every
|
||||
/// watcher automatically rebuilds against the new host with no manual
|
||||
/// invalidation elsewhere. Returns null when no connection is configured —
|
||||
/// `app_router.dart` redirects to Settings in that case rather than every
|
||||
/// screen needing its own "no connection" branch.
|
||||
|
||||
@ProviderFor(apiClient)
|
||||
const apiClientProvider = ApiClientProvider._();
|
||||
|
||||
/// Rebuilt (not mutated) whenever the active connection changes, so every
|
||||
/// watcher automatically rebuilds against the new host with no manual
|
||||
/// invalidation elsewhere. Returns null when no connection is configured —
|
||||
/// `app_router.dart` redirects to Settings in that case rather than every
|
||||
/// screen needing its own "no connection" branch.
|
||||
|
||||
final class ApiClientProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
NodeMasterApiClient?,
|
||||
NodeMasterApiClient?,
|
||||
NodeMasterApiClient?
|
||||
>
|
||||
with $Provider<NodeMasterApiClient?> {
|
||||
/// Rebuilt (not mutated) whenever the active connection changes, so every
|
||||
/// watcher automatically rebuilds against the new host with no manual
|
||||
/// invalidation elsewhere. Returns null when no connection is configured —
|
||||
/// `app_router.dart` redirects to Settings in that case rather than every
|
||||
/// screen needing its own "no connection" branch.
|
||||
const ApiClientProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'apiClientProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$apiClientHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<NodeMasterApiClient?> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
NodeMasterApiClient? create(Ref ref) {
|
||||
return apiClient(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(NodeMasterApiClient? value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<NodeMasterApiClient?>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$apiClientHash() => r'0a33d0449a51fd825f43bdcddd686bd413ff74ea';
|
||||
@@ -0,0 +1,44 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'api_exception.freezed.dart';
|
||||
|
||||
/// Typed errors thrown by [NodeMasterApiClient]/repositories. Riverpod's
|
||||
/// `AsyncValue` (via `FutureProvider`/`AsyncNotifier`) already catches
|
||||
/// thrown exceptions into `AsyncValue.error`, so this is the result-type
|
||||
/// boundary — no separate `Result<T, E>` wrapper is layered on top.
|
||||
@freezed
|
||||
sealed class ApiException with _$ApiException implements Exception {
|
||||
/// DNS/connection failure — couldn't reach the host at all.
|
||||
const factory ApiException.network(String message) = ApiNetworkException;
|
||||
|
||||
/// Connect/send/receive timeout.
|
||||
const factory ApiException.timeout() = ApiTimeoutException;
|
||||
|
||||
/// The server responded with a 4xx/5xx and (usually) a `{"error": msg}`
|
||||
/// body.
|
||||
const factory ApiException.server(int statusCode, String message) = ApiServerException;
|
||||
|
||||
/// The response body wasn't the JSON shape a model expected.
|
||||
const factory ApiException.parse(String message) = ApiParseException;
|
||||
|
||||
/// Request was cancelled (e.g. the active connection changed mid-flight).
|
||||
const factory ApiException.cancelled() = ApiCancelledException;
|
||||
|
||||
/// Anything else.
|
||||
const factory ApiException.unknown(String message) = ApiUnknownException;
|
||||
}
|
||||
|
||||
extension ApiExceptionUserMessage on ApiException {
|
||||
/// Short, user-facing copy — screens can use this directly or switch on
|
||||
/// the exception type themselves for more specific handling.
|
||||
String get userMessage => switch (this) {
|
||||
ApiNetworkException() => "Can't reach this node — check the URL and that it's reachable.",
|
||||
ApiTimeoutException() => 'The node took too long to respond.',
|
||||
ApiServerException(:final statusCode, :final message) => statusCode == 404
|
||||
? 'Not found.'
|
||||
: 'Server error ($statusCode): $message',
|
||||
ApiParseException() => 'Received an unexpected response from the node.',
|
||||
ApiCancelledException() => 'Cancelled.',
|
||||
ApiUnknownException(:final message) => message,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,528 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'api_exception.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
mixin _$ApiException {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is ApiException);
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => runtimeType.hashCode;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ApiException()';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class $ApiExceptionCopyWith<$Res> {
|
||||
$ApiExceptionCopyWith(ApiException _, $Res Function(ApiException) __);
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [ApiException].
|
||||
extension ApiExceptionPatterns on ApiException {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>({TResult Function( ApiNetworkException value)? network,TResult Function( ApiTimeoutException value)? timeout,TResult Function( ApiServerException value)? server,TResult Function( ApiParseException value)? parse,TResult Function( ApiCancelledException value)? cancelled,TResult Function( ApiUnknownException value)? unknown,required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case ApiNetworkException() when network != null:
|
||||
return network(_that);case ApiTimeoutException() when timeout != null:
|
||||
return timeout(_that);case ApiServerException() when server != null:
|
||||
return server(_that);case ApiParseException() when parse != null:
|
||||
return parse(_that);case ApiCancelledException() when cancelled != null:
|
||||
return cancelled(_that);case ApiUnknownException() when unknown != null:
|
||||
return unknown(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>({required TResult Function( ApiNetworkException value) network,required TResult Function( ApiTimeoutException value) timeout,required TResult Function( ApiServerException value) server,required TResult Function( ApiParseException value) parse,required TResult Function( ApiCancelledException value) cancelled,required TResult Function( ApiUnknownException value) unknown,}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case ApiNetworkException():
|
||||
return network(_that);case ApiTimeoutException():
|
||||
return timeout(_that);case ApiServerException():
|
||||
return server(_that);case ApiParseException():
|
||||
return parse(_that);case ApiCancelledException():
|
||||
return cancelled(_that);case ApiUnknownException():
|
||||
return unknown(_that);}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>({TResult? Function( ApiNetworkException value)? network,TResult? Function( ApiTimeoutException value)? timeout,TResult? Function( ApiServerException value)? server,TResult? Function( ApiParseException value)? parse,TResult? Function( ApiCancelledException value)? cancelled,TResult? Function( ApiUnknownException value)? unknown,}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case ApiNetworkException() when network != null:
|
||||
return network(_that);case ApiTimeoutException() when timeout != null:
|
||||
return timeout(_that);case ApiServerException() when server != null:
|
||||
return server(_that);case ApiParseException() when parse != null:
|
||||
return parse(_that);case ApiCancelledException() when cancelled != null:
|
||||
return cancelled(_that);case ApiUnknownException() when unknown != null:
|
||||
return unknown(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>({TResult Function( String message)? network,TResult Function()? timeout,TResult Function( int statusCode, String message)? server,TResult Function( String message)? parse,TResult Function()? cancelled,TResult Function( String message)? unknown,required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case ApiNetworkException() when network != null:
|
||||
return network(_that.message);case ApiTimeoutException() when timeout != null:
|
||||
return timeout();case ApiServerException() when server != null:
|
||||
return server(_that.statusCode,_that.message);case ApiParseException() when parse != null:
|
||||
return parse(_that.message);case ApiCancelledException() when cancelled != null:
|
||||
return cancelled();case ApiUnknownException() when unknown != null:
|
||||
return unknown(_that.message);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>({required TResult Function( String message) network,required TResult Function() timeout,required TResult Function( int statusCode, String message) server,required TResult Function( String message) parse,required TResult Function() cancelled,required TResult Function( String message) unknown,}) {final _that = this;
|
||||
switch (_that) {
|
||||
case ApiNetworkException():
|
||||
return network(_that.message);case ApiTimeoutException():
|
||||
return timeout();case ApiServerException():
|
||||
return server(_that.statusCode,_that.message);case ApiParseException():
|
||||
return parse(_that.message);case ApiCancelledException():
|
||||
return cancelled();case ApiUnknownException():
|
||||
return unknown(_that.message);}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>({TResult? Function( String message)? network,TResult? Function()? timeout,TResult? Function( int statusCode, String message)? server,TResult? Function( String message)? parse,TResult? Function()? cancelled,TResult? Function( String message)? unknown,}) {final _that = this;
|
||||
switch (_that) {
|
||||
case ApiNetworkException() when network != null:
|
||||
return network(_that.message);case ApiTimeoutException() when timeout != null:
|
||||
return timeout();case ApiServerException() when server != null:
|
||||
return server(_that.statusCode,_that.message);case ApiParseException() when parse != null:
|
||||
return parse(_that.message);case ApiCancelledException() when cancelled != null:
|
||||
return cancelled();case ApiUnknownException() when unknown != null:
|
||||
return unknown(_that.message);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class ApiNetworkException implements ApiException {
|
||||
const ApiNetworkException(this.message);
|
||||
|
||||
|
||||
final String message;
|
||||
|
||||
/// Create a copy of ApiException
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$ApiNetworkExceptionCopyWith<ApiNetworkException> get copyWith => _$ApiNetworkExceptionCopyWithImpl<ApiNetworkException>(this, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is ApiNetworkException&&(identical(other.message, message) || other.message == message));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,message);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ApiException.network(message: $message)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $ApiNetworkExceptionCopyWith<$Res> implements $ApiExceptionCopyWith<$Res> {
|
||||
factory $ApiNetworkExceptionCopyWith(ApiNetworkException value, $Res Function(ApiNetworkException) _then) = _$ApiNetworkExceptionCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String message
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$ApiNetworkExceptionCopyWithImpl<$Res>
|
||||
implements $ApiNetworkExceptionCopyWith<$Res> {
|
||||
_$ApiNetworkExceptionCopyWithImpl(this._self, this._then);
|
||||
|
||||
final ApiNetworkException _self;
|
||||
final $Res Function(ApiNetworkException) _then;
|
||||
|
||||
/// Create a copy of ApiException
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') $Res call({Object? message = null,}) {
|
||||
return _then(ApiNetworkException(
|
||||
null == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class ApiTimeoutException implements ApiException {
|
||||
const ApiTimeoutException();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is ApiTimeoutException);
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => runtimeType.hashCode;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ApiException.timeout()';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class ApiServerException implements ApiException {
|
||||
const ApiServerException(this.statusCode, this.message);
|
||||
|
||||
|
||||
final int statusCode;
|
||||
final String message;
|
||||
|
||||
/// Create a copy of ApiException
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$ApiServerExceptionCopyWith<ApiServerException> get copyWith => _$ApiServerExceptionCopyWithImpl<ApiServerException>(this, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is ApiServerException&&(identical(other.statusCode, statusCode) || other.statusCode == statusCode)&&(identical(other.message, message) || other.message == message));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,statusCode,message);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ApiException.server(statusCode: $statusCode, message: $message)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $ApiServerExceptionCopyWith<$Res> implements $ApiExceptionCopyWith<$Res> {
|
||||
factory $ApiServerExceptionCopyWith(ApiServerException value, $Res Function(ApiServerException) _then) = _$ApiServerExceptionCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
int statusCode, String message
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$ApiServerExceptionCopyWithImpl<$Res>
|
||||
implements $ApiServerExceptionCopyWith<$Res> {
|
||||
_$ApiServerExceptionCopyWithImpl(this._self, this._then);
|
||||
|
||||
final ApiServerException _self;
|
||||
final $Res Function(ApiServerException) _then;
|
||||
|
||||
/// Create a copy of ApiException
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') $Res call({Object? statusCode = null,Object? message = null,}) {
|
||||
return _then(ApiServerException(
|
||||
null == statusCode ? _self.statusCode : statusCode // ignore: cast_nullable_to_non_nullable
|
||||
as int,null == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class ApiParseException implements ApiException {
|
||||
const ApiParseException(this.message);
|
||||
|
||||
|
||||
final String message;
|
||||
|
||||
/// Create a copy of ApiException
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$ApiParseExceptionCopyWith<ApiParseException> get copyWith => _$ApiParseExceptionCopyWithImpl<ApiParseException>(this, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is ApiParseException&&(identical(other.message, message) || other.message == message));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,message);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ApiException.parse(message: $message)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $ApiParseExceptionCopyWith<$Res> implements $ApiExceptionCopyWith<$Res> {
|
||||
factory $ApiParseExceptionCopyWith(ApiParseException value, $Res Function(ApiParseException) _then) = _$ApiParseExceptionCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String message
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$ApiParseExceptionCopyWithImpl<$Res>
|
||||
implements $ApiParseExceptionCopyWith<$Res> {
|
||||
_$ApiParseExceptionCopyWithImpl(this._self, this._then);
|
||||
|
||||
final ApiParseException _self;
|
||||
final $Res Function(ApiParseException) _then;
|
||||
|
||||
/// Create a copy of ApiException
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') $Res call({Object? message = null,}) {
|
||||
return _then(ApiParseException(
|
||||
null == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class ApiCancelledException implements ApiException {
|
||||
const ApiCancelledException();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is ApiCancelledException);
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => runtimeType.hashCode;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ApiException.cancelled()';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class ApiUnknownException implements ApiException {
|
||||
const ApiUnknownException(this.message);
|
||||
|
||||
|
||||
final String message;
|
||||
|
||||
/// Create a copy of ApiException
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$ApiUnknownExceptionCopyWith<ApiUnknownException> get copyWith => _$ApiUnknownExceptionCopyWithImpl<ApiUnknownException>(this, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is ApiUnknownException&&(identical(other.message, message) || other.message == message));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,message);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ApiException.unknown(message: $message)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $ApiUnknownExceptionCopyWith<$Res> implements $ApiExceptionCopyWith<$Res> {
|
||||
factory $ApiUnknownExceptionCopyWith(ApiUnknownException value, $Res Function(ApiUnknownException) _then) = _$ApiUnknownExceptionCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String message
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$ApiUnknownExceptionCopyWithImpl<$Res>
|
||||
implements $ApiUnknownExceptionCopyWith<$Res> {
|
||||
_$ApiUnknownExceptionCopyWithImpl(this._self, this._then);
|
||||
|
||||
final ApiUnknownException _self;
|
||||
final $Res Function(ApiUnknownException) _then;
|
||||
|
||||
/// Create a copy of ApiException
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') $Res call({Object? message = null,}) {
|
||||
return _then(ApiUnknownException(
|
||||
null == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
@@ -0,0 +1,39 @@
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import 'api_exception.dart';
|
||||
|
||||
/// Maps a caught [DioException] (or any other error) to a typed
|
||||
/// [ApiException]. The one place this translation happens — repositories
|
||||
/// and the API client both funnel through this rather than inspecting
|
||||
/// `DioException` directly.
|
||||
ApiException mapDioException(Object error) {
|
||||
if (error is ApiException) return error;
|
||||
|
||||
if (error is! DioException) {
|
||||
return ApiException.unknown(error.toString());
|
||||
}
|
||||
|
||||
switch (error.type) {
|
||||
case DioExceptionType.connectionTimeout:
|
||||
case DioExceptionType.sendTimeout:
|
||||
case DioExceptionType.receiveTimeout:
|
||||
case DioExceptionType.transformTimeout:
|
||||
return const ApiException.timeout();
|
||||
case DioExceptionType.connectionError:
|
||||
return ApiException.network(error.message ?? 'Connection failed.');
|
||||
case DioExceptionType.cancel:
|
||||
return const ApiException.cancelled();
|
||||
case DioExceptionType.badResponse:
|
||||
final response = error.response;
|
||||
final statusCode = response?.statusCode ?? 0;
|
||||
final data = response?.data;
|
||||
final message = data is Map && data['error'] is String
|
||||
? data['error'] as String
|
||||
: (response?.statusMessage ?? 'Request failed.');
|
||||
return ApiException.server(statusCode, message);
|
||||
case DioExceptionType.badCertificate:
|
||||
return ApiException.network('Bad certificate: ${error.message ?? ''}');
|
||||
case DioExceptionType.unknown:
|
||||
return ApiException.unknown(error.message ?? error.toString());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import '../models/models.dart';
|
||||
import 'api_exception.dart';
|
||||
import 'dio_error_mapper.dart';
|
||||
|
||||
/// Thin typed wrapper over [Dio], one method per NodeMaster REST endpoint.
|
||||
/// Every method funnels errors through [mapDioException] so callers only
|
||||
/// ever see [ApiException]. Holds a [CancelToken] tied to the client's own
|
||||
/// lifetime (cancelled by [apiClientProvider] when the active connection
|
||||
/// changes), so a request from an abandoned connection can't resolve into
|
||||
/// the next connection's UI state.
|
||||
class NodeMasterApiClient {
|
||||
NodeMasterApiClient(this._dio, this._cancelToken);
|
||||
|
||||
final Dio _dio;
|
||||
final CancelToken _cancelToken;
|
||||
|
||||
/// The overall budget for one request, enforced by Dart's own
|
||||
/// `Future.timeout` rather than relying solely on Dio's
|
||||
/// `connectTimeout`/`receiveTimeout` `BaseOptions`. Dio's browser HTTP
|
||||
/// adapter has no socket-level hooks to honor those (the underlying
|
||||
/// `fetch`/`XMLHttpRequest` APIs don't expose a connect-phase timeout),
|
||||
/// so on web a request to an unreachable host would otherwise hang
|
||||
/// until the browser's own (much longer, sometimes unbounded) default —
|
||||
/// this timeout is what actually bounds it on every platform.
|
||||
static const _requestBudget = Duration(seconds: 20);
|
||||
|
||||
Future<T> _guard<T>(Future<T> Function() body) async {
|
||||
try {
|
||||
return await body().timeout(_requestBudget);
|
||||
} on ApiException {
|
||||
rethrow;
|
||||
} on DioException catch (e) {
|
||||
throw mapDioException(e);
|
||||
} on TimeoutException {
|
||||
throw const ApiException.timeout();
|
||||
} on TypeError catch (e) {
|
||||
throw ApiException.parse(e.toString());
|
||||
} on FormatException catch (e) {
|
||||
throw ApiException.parse(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
Future<Response<dynamic>> _get(String path, {Map<String, dynamic>? query}) => _dio.get(
|
||||
path,
|
||||
queryParameters: query,
|
||||
cancelToken: _cancelToken,
|
||||
);
|
||||
|
||||
Future<Response<dynamic>> _post(String path, {Object? data}) => _dio.post(
|
||||
path,
|
||||
data: data,
|
||||
cancelToken: _cancelToken,
|
||||
);
|
||||
|
||||
Future<Response<dynamic>> _put(String path, {Object? data}) => _dio.put(
|
||||
path,
|
||||
data: data,
|
||||
cancelToken: _cancelToken,
|
||||
);
|
||||
|
||||
Future<Response<dynamic>> _delete(String path) => _dio.delete(path, cancelToken: _cancelToken);
|
||||
|
||||
// ---- node ----
|
||||
|
||||
Future<NodeInfo> getNode() => _guard(() async {
|
||||
final res = await _get('/node/');
|
||||
return NodeInfo.fromJson(res.data as Map<String, dynamic>);
|
||||
});
|
||||
|
||||
Future<void> updateNode(NodeInfo node) => _guard(() async {
|
||||
await _put('/node/', data: node.toJson());
|
||||
});
|
||||
|
||||
Future<BackupConfig> getNodeBackup() => _guard(() async {
|
||||
final res = await _get('/node/backup');
|
||||
return BackupConfig.fromJson(res.data as Map<String, dynamic>);
|
||||
});
|
||||
|
||||
Future<void> updateNodeBackup(BackupConfig config) => _guard(() async {
|
||||
await _put('/node/backup', data: config.toJson());
|
||||
});
|
||||
|
||||
Future<void> runNodeBackup() => _guard(() async {
|
||||
await _post('/node/backup/run');
|
||||
});
|
||||
|
||||
Future<List<BackupTarget>> getNodeBackupTargets() => _guard(() async {
|
||||
final res = await _get('/node/backup/targets');
|
||||
return (res.data as List<dynamic>)
|
||||
.map((e) => BackupTarget.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
});
|
||||
|
||||
Future<BackupTarget> addNodeBackupTarget(BackupTarget target) => _guard(() async {
|
||||
final res = await _post('/node/backup/targets', data: target.toJson());
|
||||
return BackupTarget.fromJson(res.data as Map<String, dynamic>);
|
||||
});
|
||||
|
||||
Future<BackupTarget> updateNodeBackupTarget(String targetId, BackupTarget target) => _guard(() async {
|
||||
final res = await _put('/node/backup/targets/$targetId', data: target.toJson());
|
||||
return BackupTarget.fromJson(res.data as Map<String, dynamic>);
|
||||
});
|
||||
|
||||
Future<void> deleteNodeBackupTarget(String targetId) => _guard(() async {
|
||||
await _delete('/node/backup/targets/$targetId');
|
||||
});
|
||||
|
||||
Future<void> runNodeBackupTarget(String targetId) => _guard(() async {
|
||||
await _post('/node/backup/targets/$targetId/run');
|
||||
});
|
||||
|
||||
Future<RunState> getNodeBackupTargetProgress(String targetId) => _guard(() async {
|
||||
final res = await _get('/node/backup/targets/$targetId/progress');
|
||||
return RunState.fromJson(res.data as Map<String, dynamic>);
|
||||
});
|
||||
|
||||
Future<List<RunState>> getNodeBackupProgress() => _guard(() async {
|
||||
final res = await _get('/node/backup/progress');
|
||||
return (res.data as List<dynamic>)
|
||||
.map((e) => RunState.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
});
|
||||
|
||||
Future<ScanResult> scan({String? folder}) => _guard(() async {
|
||||
final res = await _post('/node/scan', data: folder == null ? {} : {'folder': folder});
|
||||
return ScanResult.fromJson(res.data as Map<String, dynamic>);
|
||||
});
|
||||
|
||||
Future<List<UpdateRecord>> getUpdates() => _guard(() async {
|
||||
final res = await _get('/node/updates');
|
||||
return (res.data as List<dynamic>)
|
||||
.map((e) => UpdateRecord.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
});
|
||||
|
||||
Future<UpdateRecord> addUpdate(UpdateRecord record) => _guard(() async {
|
||||
final res = await _post('/node/updates', data: record.toJson());
|
||||
return UpdateRecord.fromJson(res.data as Map<String, dynamic>);
|
||||
});
|
||||
|
||||
Future<void> deleteUpdate(String id) => _guard(() async {
|
||||
await _delete('/node/updates/$id');
|
||||
});
|
||||
|
||||
// ---- nodes (fleet registry) ----
|
||||
|
||||
Future<List<RemoteNode>> getAllRemoteNodes() => _guard(() async {
|
||||
final res = await _get('/nodes/');
|
||||
return (res.data as List<dynamic>)
|
||||
.map((e) => RemoteNode.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
});
|
||||
|
||||
Future<RemoteNode> addRemoteNode(RemoteNode node) => _guard(() async {
|
||||
final res = await _post('/nodes/', data: node.toJson());
|
||||
return RemoteNode.fromJson(res.data as Map<String, dynamic>);
|
||||
});
|
||||
|
||||
Future<RemoteNode> getRemoteNode(String id) => _guard(() async {
|
||||
final res = await _get('/nodes/$id');
|
||||
return RemoteNode.fromJson(res.data as Map<String, dynamic>);
|
||||
});
|
||||
|
||||
Future<RemoteNode> updateRemoteNode(String id, RemoteNode node) => _guard(() async {
|
||||
final res = await _put('/nodes/$id', data: node.toJson());
|
||||
return RemoteNode.fromJson(res.data as Map<String, dynamic>);
|
||||
});
|
||||
|
||||
Future<void> deleteRemoteNode(String id) => _guard(() async {
|
||||
await _delete('/nodes/$id');
|
||||
});
|
||||
|
||||
Future<List<AggregatedNodeStatus>> getAggregated() => _guard(() async {
|
||||
final res = await _get('/nodes/aggregated');
|
||||
return (res.data as List<dynamic>)
|
||||
.map((e) => AggregatedNodeStatus.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
});
|
||||
|
||||
// ---- services ----
|
||||
|
||||
Future<List<Service>> getAllServices() => _guard(() async {
|
||||
final res = await _get('/services/');
|
||||
return (res.data as List<dynamic>)
|
||||
.map((e) => Service.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
});
|
||||
|
||||
Future<Service> addService(Service service) => _guard(() async {
|
||||
final res = await _post('/services/', data: service.toJson());
|
||||
return Service.fromJson(res.data as Map<String, dynamic>);
|
||||
});
|
||||
|
||||
Future<Service> getService(String id) => _guard(() async {
|
||||
final res = await _get('/services/$id');
|
||||
return Service.fromJson(res.data as Map<String, dynamic>);
|
||||
});
|
||||
|
||||
Future<Service> updateService(String id, Service service) => _guard(() async {
|
||||
final res = await _put('/services/$id', data: service.toJson());
|
||||
return Service.fromJson(res.data as Map<String, dynamic>);
|
||||
});
|
||||
|
||||
Future<void> deleteService(String id) => _guard(() async {
|
||||
await _delete('/services/$id');
|
||||
});
|
||||
|
||||
Future<void> startService(String id) => _guard(() async {
|
||||
await _post('/services/$id/start');
|
||||
});
|
||||
|
||||
Future<void> stopService(String id) => _guard(() async {
|
||||
await _post('/services/$id/stop');
|
||||
});
|
||||
|
||||
Future<void> backupService(String id) => _guard(() async {
|
||||
await _post('/services/$id/backup');
|
||||
});
|
||||
|
||||
Future<List<BackupTarget>> getServiceBackupTargets(String id) => _guard(() async {
|
||||
final res = await _get('/services/$id/backup/targets');
|
||||
return (res.data as List<dynamic>)
|
||||
.map((e) => BackupTarget.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
});
|
||||
|
||||
Future<BackupTarget> addServiceBackupTarget(String id, BackupTarget target) => _guard(() async {
|
||||
final res = await _post('/services/$id/backup/targets', data: target.toJson());
|
||||
return BackupTarget.fromJson(res.data as Map<String, dynamic>);
|
||||
});
|
||||
|
||||
Future<BackupTarget> updateServiceBackupTarget(String id, String targetId, BackupTarget target) =>
|
||||
_guard(() async {
|
||||
final res = await _put('/services/$id/backup/targets/$targetId', data: target.toJson());
|
||||
return BackupTarget.fromJson(res.data as Map<String, dynamic>);
|
||||
});
|
||||
|
||||
Future<void> deleteServiceBackupTarget(String id, String targetId) => _guard(() async {
|
||||
await _delete('/services/$id/backup/targets/$targetId');
|
||||
});
|
||||
|
||||
Future<void> runServiceBackupTarget(String id, String targetId) => _guard(() async {
|
||||
await _post('/services/$id/backup/targets/$targetId/run');
|
||||
});
|
||||
|
||||
Future<RunState> getServiceBackupTargetProgress(String id, String targetId) => _guard(() async {
|
||||
final res = await _get('/services/$id/backup/targets/$targetId/progress');
|
||||
return RunState.fromJson(res.data as Map<String, dynamic>);
|
||||
});
|
||||
|
||||
Future<List<RunState>> getServiceBackupProgress(String id) => _guard(() async {
|
||||
final res = await _get('/services/$id/backup/progress');
|
||||
return (res.data as List<dynamic>)
|
||||
.map((e) => RunState.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user