Browse Source

refactor(neon): make RequestManager more reliable and performant

Signed-off-by: Nikolas Rimikis <leptopoda@users.noreply.github.com>
pull/846/head
Nikolas Rimikis 1 year ago
parent
commit
7ed9d1b66f
No known key found for this signature in database
GPG Key ID: 85ED1DE9786A4FF2
  1. 18
      packages/neon/neon/lib/src/bloc/result.dart
  2. 154
      packages/neon/neon/lib/src/utils/request_manager.dart

18
packages/neon/neon/lib/src/bloc/result.dart

@ -42,11 +42,19 @@ class Result<T> {
isCached: isCached, isCached: isCached,
); );
Result<T> asLoading() => Result( Result<T> asLoading() => copyWith(isLoading: true);
data,
error, Result<T> copyWith({
isLoading: true, final T? data,
isCached: isCached, final Object? error,
final bool? isLoading,
final bool? isCached,
}) =>
Result(
data ?? this.data,
error ?? this.error,
isLoading: isLoading ?? this.isLoading,
isCached: isCached ?? this.isCached,
); );
bool get hasError => error != null; bool get hasError => error != null;

154
packages/neon/neon/lib/src/utils/request_manager.dart

@ -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(
_emitCached(
key, key,
subject, subject,
unwrap, unwrap,
deserialize, deserialize,
emitEmptyCache, emitEmptyCache,
true, ),
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,20 +145,21 @@ 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>(
@ -158,36 +168,56 @@ class RequestManager {
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;
try {
if (_cache != null && await _cache!.has(key)) { if (_cache != null && await _cache!.has(key)) {
cached = unwrap(await compute(deserialize, (await _cache!.get(key))!)); try {
final cacheValue = 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) { } catch (e, s) {
debugPrint(e.toString()); debugPrint(e.toString());
debugPrint(s.toString()); debugPrintStack(stackTrace: s, maxFrames: 5);
}
} }
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

Loading…
Cancel
Save