|
|
@ -1,3 +1,4 @@ |
|
|
|
|
|
|
|
import 'dart:async'; |
|
|
|
import 'dart:convert'; |
|
|
|
import 'dart:convert'; |
|
|
|
|
|
|
|
|
|
|
|
import 'package:flutter/foundation.dart'; |
|
|
|
import 'package:flutter/foundation.dart'; |
|
|
@ -13,7 +14,10 @@ import 'package:xml/xml.dart' as xml; |
|
|
|
typedef UnwrapCallback<T, R> = T Function(R); |
|
|
|
typedef UnwrapCallback<T, R> = T Function(R); |
|
|
|
typedef SerializeCallback<T> = String Function(T); |
|
|
|
typedef SerializeCallback<T> = String Function(T); |
|
|
|
typedef DeserializeCallback<T> = T Function(String); |
|
|
|
typedef DeserializeCallback<T> = T Function(String); |
|
|
|
typedef NextcloudApiCallback<T> = Future<T> Function(); |
|
|
|
typedef NextcloudApiCallback<T> = AsyncValueGetter<T>; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const maxRetries = 3; |
|
|
|
|
|
|
|
const defaultTimeout = Duration(seconds: 30); |
|
|
|
|
|
|
|
|
|
|
|
class RequestManager { |
|
|
|
class RequestManager { |
|
|
|
RequestManager(); |
|
|
|
RequestManager(); |
|
|
@ -34,26 +38,32 @@ class RequestManager { |
|
|
|
|
|
|
|
|
|
|
|
Cache? _cache; |
|
|
|
Cache? _cache; |
|
|
|
|
|
|
|
|
|
|
|
Future<void> wrapNextcloud<T, R>( |
|
|
|
Future<void> wrapNextcloud<T, B, H>( |
|
|
|
final String clientID, |
|
|
|
final String clientID, |
|
|
|
final String k, |
|
|
|
final String k, |
|
|
|
final BehaviorSubject<Result<T>> subject, |
|
|
|
final BehaviorSubject<Result<T>> subject, |
|
|
|
final NextcloudApiCallback<R> call, |
|
|
|
final DynamiteRawResponse<B, H> rawResponse, |
|
|
|
final UnwrapCallback<T, R> unwrap, { |
|
|
|
final UnwrapCallback<T, DynamiteResponse<B, H>> unwrap, { |
|
|
|
final bool disableTimeout = false, |
|
|
|
final bool disableTimeout = false, |
|
|
|
final bool emitEmptyCache = false, |
|
|
|
|
|
|
|
}) async => |
|
|
|
}) async => |
|
|
|
_wrap<T, R>( |
|
|
|
_wrap<T, DynamiteRawResponse<B, H>>( |
|
|
|
clientID, |
|
|
|
clientID, |
|
|
|
k, |
|
|
|
k, |
|
|
|
subject, |
|
|
|
subject, |
|
|
|
call, |
|
|
|
() async { |
|
|
|
unwrap, |
|
|
|
await rawResponse.future; |
|
|
|
(final data) => json.encode(serializeNextcloud<R>(data)), |
|
|
|
|
|
|
|
(final data) => deserializeNextcloud<R>(json.decode(data) as Object), |
|
|
|
return rawResponse; |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
(final rawResponse) => unwrap(rawResponse.response), |
|
|
|
|
|
|
|
(final data) => json.encode(data), |
|
|
|
|
|
|
|
(final data) => DynamiteRawResponse<B, H>.fromJson( |
|
|
|
|
|
|
|
json.decode(data) as Map<String, Object?>, |
|
|
|
|
|
|
|
serializers: rawResponse.serializers, |
|
|
|
|
|
|
|
bodyType: rawResponse.bodyType, |
|
|
|
|
|
|
|
headersType: rawResponse.headersType, |
|
|
|
|
|
|
|
), |
|
|
|
disableTimeout, |
|
|
|
disableTimeout, |
|
|
|
emitEmptyCache, |
|
|
|
|
|
|
|
0, |
|
|
|
|
|
|
|
); |
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
Future<void> wrapWebDav<T>( |
|
|
|
Future<void> wrapWebDav<T>( |
|
|
@ -75,7 +85,6 @@ class RequestManager { |
|
|
|
(final data) => WebDavMultistatus.fromXmlElement(xml.XmlDocument.parse(data).rootElement), |
|
|
|
(final data) => WebDavMultistatus.fromXmlElement(xml.XmlDocument.parse(data).rootElement), |
|
|
|
disableTimeout, |
|
|
|
disableTimeout, |
|
|
|
emitEmptyCache, |
|
|
|
emitEmptyCache, |
|
|
|
0, |
|
|
|
|
|
|
|
); |
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
Future<void> _wrap<T, R>( |
|
|
|
Future<void> _wrap<T, R>( |
|
|
@ -85,44 +94,44 @@ class RequestManager { |
|
|
|
final NextcloudApiCallback<R> call, |
|
|
|
final NextcloudApiCallback<R> call, |
|
|
|
final UnwrapCallback<T, R> unwrap, |
|
|
|
final UnwrapCallback<T, R> unwrap, |
|
|
|
final SerializeCallback<R> serialize, |
|
|
|
final SerializeCallback<R> serialize, |
|
|
|
final DeserializeCallback<R> deserialize, |
|
|
|
final DeserializeCallback<R> deserialize, [ |
|
|
|
final bool disableTimeout, |
|
|
|
final bool disableTimeout = false, |
|
|
|
final bool emitEmptyCache, |
|
|
|
final bool emitEmptyCache = false, |
|
|
|
final int retries, |
|
|
|
final int retries = 0, |
|
|
|
) async { |
|
|
|
]) async { |
|
|
|
if (subject.valueOrNull?.hasData ?? false) { |
|
|
|
// emit loading state with the current value if present |
|
|
|
subject.add( |
|
|
|
final value = subject.valueOrNull?.copyWith(isLoading: true) ?? Result.loading(); |
|
|
|
Result( |
|
|
|
subject.add(value); |
|
|
|
subject.value.data, |
|
|
|
|
|
|
|
null, |
|
|
|
|
|
|
|
isLoading: true, |
|
|
|
|
|
|
|
isCached: true, |
|
|
|
|
|
|
|
), |
|
|
|
|
|
|
|
); |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
subject.add(Result.loading()); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
final key = '$clientID-$k'; |
|
|
|
final key = '$clientID-$k'; |
|
|
|
|
|
|
|
|
|
|
|
await _emitCached( |
|
|
|
unawaited( |
|
|
|
key, |
|
|
|
_emitCached( |
|
|
|
subject, |
|
|
|
key, |
|
|
|
unwrap, |
|
|
|
subject, |
|
|
|
deserialize, |
|
|
|
unwrap, |
|
|
|
emitEmptyCache, |
|
|
|
deserialize, |
|
|
|
true, |
|
|
|
emitEmptyCache, |
|
|
|
null, |
|
|
|
), |
|
|
|
); |
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
try { |
|
|
|
try { |
|
|
|
final response = await (disableTimeout ? call() : timeout(call)); |
|
|
|
final response = await timeout(call, disableTimeout: disableTimeout); |
|
|
|
await _cache?.set(key, await compute(serialize, response)); |
|
|
|
|
|
|
|
subject.add(Result.success(unwrap(response))); |
|
|
|
subject.add(Result.success(unwrap(response))); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
final serialized = serialize(response); |
|
|
|
|
|
|
|
await _cache?.set(key, serialized); |
|
|
|
|
|
|
|
} on TimeoutException catch (e, s) { |
|
|
|
|
|
|
|
debugPrint('Request timed out ...'); |
|
|
|
|
|
|
|
debugPrint(e.toString()); |
|
|
|
|
|
|
|
debugPrintStack(stackTrace: s, maxFrames: 5); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_emitError<T>(e, subject); |
|
|
|
} catch (e, s) { |
|
|
|
} catch (e, s) { |
|
|
|
debugPrint(e.toString()); |
|
|
|
debugPrint(e.toString()); |
|
|
|
debugPrint(s.toString()); |
|
|
|
debugPrintStack(stackTrace: s, maxFrames: 5); |
|
|
|
if (e is DynamiteApiException && e.statusCode >= 500 && retries < 3) { |
|
|
|
|
|
|
|
|
|
|
|
if (e is DynamiteApiException && e.statusCode >= 500 && retries < maxRetries) { |
|
|
|
debugPrint('Retrying...'); |
|
|
|
debugPrint('Retrying...'); |
|
|
|
await _wrap( |
|
|
|
await _wrap( |
|
|
|
clientID, |
|
|
|
clientID, |
|
|
@ -136,58 +145,79 @@ class RequestManager { |
|
|
|
emitEmptyCache, |
|
|
|
emitEmptyCache, |
|
|
|
retries + 1, |
|
|
|
retries + 1, |
|
|
|
); |
|
|
|
); |
|
|
|
return; |
|
|
|
} else { |
|
|
|
} |
|
|
|
_emitError<T>(e, subject); |
|
|
|
if (!(await _emitCached( |
|
|
|
|
|
|
|
key, |
|
|
|
|
|
|
|
subject, |
|
|
|
|
|
|
|
unwrap, |
|
|
|
|
|
|
|
deserialize, |
|
|
|
|
|
|
|
emitEmptyCache, |
|
|
|
|
|
|
|
false, |
|
|
|
|
|
|
|
e, |
|
|
|
|
|
|
|
))) { |
|
|
|
|
|
|
|
subject.add(Result.error(e)); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Re emits the current result with the given [error]. |
|
|
|
|
|
|
|
/// |
|
|
|
|
|
|
|
/// Defaults to a [Result.error] if none has been emitted yet. |
|
|
|
|
|
|
|
void _emitError<T>( |
|
|
|
|
|
|
|
final Object error, |
|
|
|
|
|
|
|
final BehaviorSubject<Result<T>> subject, |
|
|
|
|
|
|
|
) { |
|
|
|
|
|
|
|
final value = subject.valueOrNull?.copyWith(error: error, isLoading: false) ?? Result.error(error); |
|
|
|
|
|
|
|
subject.add(value); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
Future<bool> _emitCached<T, R>( |
|
|
|
Future<bool> _emitCached<T, R>( |
|
|
|
final String key, |
|
|
|
final String key, |
|
|
|
final BehaviorSubject<Result<T>> subject, |
|
|
|
final BehaviorSubject<Result<T>> subject, |
|
|
|
final UnwrapCallback<T, R> unwrap, |
|
|
|
final UnwrapCallback<T, R> unwrap, |
|
|
|
final DeserializeCallback<R> deserialize, |
|
|
|
final DeserializeCallback<R> deserialize, |
|
|
|
final bool emitEmptyCache, |
|
|
|
final bool emitEmptyCache, |
|
|
|
final bool loading, |
|
|
|
|
|
|
|
final Object? error, |
|
|
|
|
|
|
|
) async { |
|
|
|
) async { |
|
|
|
T? cached; |
|
|
|
if (_cache != null && await _cache!.has(key)) { |
|
|
|
try { |
|
|
|
try { |
|
|
|
if (_cache != null && await _cache!.has(key)) { |
|
|
|
final cacheValue = await _cache!.get(key); |
|
|
|
cached = unwrap(await compute(deserialize, (await _cache!.get(key))!)); |
|
|
|
final cached = unwrap(deserialize(cacheValue!)); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// If the network fetch is faster than fetching the cached value the |
|
|
|
|
|
|
|
// subject can be closed before emitting. |
|
|
|
|
|
|
|
if (subject.value.hasUncachedData) { |
|
|
|
|
|
|
|
return true; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
subject.add( |
|
|
|
|
|
|
|
subject.value.copyWith( |
|
|
|
|
|
|
|
data: cached, |
|
|
|
|
|
|
|
isCached: true, |
|
|
|
|
|
|
|
), |
|
|
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return true; |
|
|
|
|
|
|
|
} catch (e, s) { |
|
|
|
|
|
|
|
debugPrint(e.toString()); |
|
|
|
|
|
|
|
debugPrintStack(stackTrace: s, maxFrames: 5); |
|
|
|
} |
|
|
|
} |
|
|
|
} catch (e, s) { |
|
|
|
|
|
|
|
debugPrint(e.toString()); |
|
|
|
|
|
|
|
debugPrint(s.toString()); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
if (cached != null || emitEmptyCache) { |
|
|
|
|
|
|
|
|
|
|
|
if (emitEmptyCache && !subject.value.hasUncachedData) { |
|
|
|
subject.add( |
|
|
|
subject.add( |
|
|
|
Result( |
|
|
|
subject.value.copyWith( |
|
|
|
cached, |
|
|
|
|
|
|
|
error, |
|
|
|
|
|
|
|
isLoading: loading, |
|
|
|
|
|
|
|
isCached: true, |
|
|
|
isCached: true, |
|
|
|
), |
|
|
|
), |
|
|
|
); |
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
return true; |
|
|
|
return true; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return false; |
|
|
|
return false; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
Future<T> timeout<T>( |
|
|
|
Future<T> timeout<T>( |
|
|
|
final NextcloudApiCallback<T> call, |
|
|
|
final NextcloudApiCallback<T> call, { |
|
|
|
) => |
|
|
|
final bool disableTimeout = false, |
|
|
|
call().timeout(const Duration(seconds: 30)); |
|
|
|
final Duration timeout = const Duration(seconds: 30), |
|
|
|
|
|
|
|
}) { |
|
|
|
|
|
|
|
if (disableTimeout) { |
|
|
|
|
|
|
|
return call(); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return call().timeout(timeout); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@internal |
|
|
|
@internal |
|
|
|