part of '../neon.dart'; class RequestManager { RequestManager([ this.cache, ]); final Cache? cache; Stream> wrapWithoutCache( final Future Function() call, { final bool disableTimeout = false, }) async* { yield Result.loading(); try { yield Result.success(await _timeout(disableTimeout, call)); } on Exception catch (e) { debugPrint(e.toString()); yield Result.error(e); } } Stream> wrapNextcloud( final String clientID, final String k, final Future Function() call, final T Function(R) unwrap, { required final T? previousData, final bool disableTimeout = false, }) async* { if (previousData != null) { yield ResultCached(previousData, loading: true); } else { yield Result.loading(); } final key = '$clientID-$k'; if (cache != null && await cache!.has(key)) { yield ResultCached(unwrap(deserialize(json.decode((await cache!.get(key))!))), loading: true); } try { final response = await _timeout(disableTimeout, call); await cache?.set(key, json.encode(serialize(response))); yield Result.success(unwrap(response)); } on Exception catch (e) { if (cache != null && await cache!.has(key)) { debugPrint(e.toString()); yield ResultCached(unwrap(deserialize(json.decode((await cache!.get(key))!))), error: e); return; } debugPrint(e.toString()); yield Result.error(e); } } Future _timeout( final bool disable, final Future Function() call, ) => disable ? call() : call().timeout(const Duration(seconds: 30)); } class Cache { Cache(this._platform); final NeonPlatform _platform; Database? _database; Future init() async { if (_database != null) { return; } _database = await openDatabase( p.join( await _platform.getApplicationCachePath(), 'cache.db', ), version: 1, onCreate: (final db, final version) async { await db.execute('CREATE TABLE cache (id INTEGER PRIMARY KEY, key TEXT, value TEXT, UNIQUE(key))'); }, ); } Future has(final String key) async => Sqflite.firstIntValue(await _database!.rawQuery('SELECT COUNT(*) FROM cache WHERE key = ?', [key])) == 1; Future get(final String key) async => (await _database!.rawQuery('SELECT value FROM cache WHERE key = ?', [key]))[0]['value'] as String?; Future set(final String key, final String value) async => _database!.rawQuery( 'INSERT INTO cache (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value', [key, value], ); } class ResultCached implements Result { ResultCached( this.data, { this.error, this.loading = false, this.tag = '', }); final T data; final Exception? error; final bool loading; @override // ignore: avoid_equals_and_hash_code_on_mutable_classes bool operator ==(final dynamic other) { if (other is! ResultCached) { return false; } // Compare list if (other.data is List && data is List) { // ignore: no_leading_underscores_for_local_identifiers final _otherData = other.data as List; // ignore: no_leading_underscores_for_local_identifiers final _data = data as List; return other.tag == tag && const ListEquality().equals(_otherData, _data); } return other.tag == tag && other.data == data && other.error.toString() == error.toString(); } @override String toString() => 'ResultCached(data: $data, tag: $tag)'; @override // ignore: avoid_equals_and_hash_code_on_mutable_classes int get hashCode => T.hashCode ^ tag.hashCode; @override String tag; } extension ResultDataError on Result { T? get data { if (this is ResultSuccess) { return (this as ResultSuccess).data; } if (this is ResultCached) { return (this as ResultCached).data; } return null; } Exception? get error { if (this is ResultError) { return (this as ResultError).error; } if (this is ResultCached) { return (this as ResultCached).error; } return null; } }