implemented background prefetching for files root, added 24-hour caching for root directory listing, and enabled cache renewal for manual refreshes

This commit is contained in:
2026-05-09 23:39:06 +02:00
parent 14090b96f4
commit bf28a678c9
4 changed files with 67 additions and 3 deletions
@@ -16,8 +16,9 @@ class ListFilesCache extends SimpleCache<ListFilesResponse> {
super.onNetworkData, super.onNetworkData,
super.onError, super.onError,
required String path, required String path,
super.renew = false,
}) : super( }) : super(
cacheTime: RequestCache.cacheNothing, cacheTime: _cacheTimeFor(path),
loader: () => ListFiles(ListFilesParams(path)).run(), loader: () => ListFiles(ListFilesParams(path)).run(),
fromJson: ListFilesResponse.fromJson, fromJson: ListFilesResponse.fromJson,
onUpdate: onUpdate, onUpdate: onUpdate,
@@ -25,6 +26,44 @@ class ListFilesCache extends SimpleCache<ListFilesResponse> {
start(_documentId(path)); start(_documentId(path));
} }
/// The Nextcloud root listing is significantly slower than subfolders on
/// our instance and frequently returns HTTP 500. Since its content rarely
/// changes, the root payload is cached for a full day so app-resume and
/// connectivity-change auto-refetch triggers do not re-hit the slow root
/// endpoint within the same day. To avoid a long wait on the very first
/// open of the Files page, `prefetchRootListing` (called from `main`)
/// kicks off an async warm-up fetch in the background while the user is
/// still on the launch screen / other modules. Subfolders keep the
/// previous "always refetch on visit" TTL because their content changes
/// more often. Explicit user refreshes (rename, delete, copy/move,
/// upload) bypass the TTL via the inherited [renew] flag or via
/// [invalidate].
static int _cacheTimeFor(String path) {
final stripped = path.replaceAll('/', '').trim();
return stripped.isEmpty
? RequestCache.cacheDay
: RequestCache.cacheNothing;
}
/// Triggers a root-listing fetch in the background if no cached payload
/// exists yet. Intended to be called once after login from `main` so the
/// (slow) root listing is already populated by the time the user
/// navigates to the Files module.
///
/// No-ops when a cached root payload is already present in localstore —
/// the regular TTL handling in [RequestCache] takes over from there.
static Future<void> prefetchRootListing() async {
const rootPath = '';
final cached = await Localstore.instance
.collection(RequestCache.collection)
.doc(_documentId(rootPath))
.get();
if (cached != null) return;
// Drive the same code path as a regular fetch so the result lands in
// the cache; we don't care about the in-memory callback here.
ListFilesCache(path: rootPath, onUpdate: (_) {});
}
static String _documentId(String path) { static String _documentId(String path) {
final cacheName = md5 final cacheName = md5
.convert(utf8.encode('MarianumMobile-$path')) .convert(utf8.encode('MarianumMobile-$path'))
+15
View File
@@ -17,6 +17,7 @@ import 'package:path_provider/path_provider.dart';
import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart'; import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'api/marianumcloud/webdav/queries/list_files/list_files_cache.dart';
import 'api/mhsl/breaker/get_breakers/get_breakers_response.dart'; import 'api/mhsl/breaker/get_breakers/get_breakers_response.dart';
import 'app.dart'; import 'app.dart';
import 'background/widget_background_task.dart'; import 'background/widget_background_task.dart';
@@ -91,6 +92,20 @@ Future<void> main() async {
), ),
); );
// Warm up the Nextcloud root listing in the background while the user is
// still on the launch screen / other modules — the root endpoint is slow
// on our instance, so kicking it off early means the Files page already
// has data ready by the time the user navigates to it. No-op when a
// cached payload is already present, so this does not undo the day-long
// root cache TTL.
if (AccountData().isPopulated()) {
unawaited(
ListFilesCache.prefetchRootListing().onError(
(e, _) => log('Files root prefetch failed: $e'),
),
);
}
if (kReleaseMode) { if (kReleaseMode) {
ErrorWidget.builder = (error) => Material( ErrorWidget.builder = (error) => Material(
color: Colors.white, color: Colors.white,
@@ -37,7 +37,9 @@ class FilesBloc
Future<void> refresh() async { Future<void> refresh() async {
add(RefetchStarted<FilesState>()); add(RefetchStarted<FilesState>());
final path = innerState?.currentPath ?? initialPath; final path = innerState?.currentPath ?? initialPath;
await _query(path); // Explicit user action — bypass the cache TTL so the root listing also
// refetches even though it is otherwise cached for a day.
await _query(path, renew: true);
} }
Future<void> setPath(List<String> path) async { Future<void> setPath(List<String> path) async {
@@ -52,7 +54,7 @@ class FilesBloc
await refresh(); await refresh();
} }
Future<void> _query(List<String> path) async { Future<void> _query(List<String> path, {bool renew = false}) async {
final pathString = path.isEmpty ? '/' : path.join('/'); final pathString = path.isEmpty ? '/' : path.join('/');
// Drop late results when [setPath] has navigated elsewhere or when the // Drop late results when [setPath] has navigated elsewhere or when the
@@ -71,6 +73,7 @@ class FilesBloc
try { try {
listing = await repo.data.listFiles( listing = await repo.data.listFiles(
pathString, pathString,
renew: renew,
onCacheData: (cached) { onCacheData: (cached) {
if (isStale()) return; if (isStale()) return;
// Cached payload arrives before the network call settles. Surface it // Cached payload arrives before the network call settles. Surface it
@@ -11,16 +11,23 @@ class FilesDataProvider {
/// network call is still pending. The Future itself resolves once both the /// network call is still pending. The Future itself resolves once both the
/// cache lookup and the network attempt have settled, throwing if no payload /// cache lookup and the network attempt have settled, throwing if no payload
/// could be obtained at all. /// could be obtained at all.
///
/// Pass [renew] for explicit user-triggered reloads (pull-to-refresh, after
/// a rename / delete / move / upload). It bypasses the per-path TTL in
/// [ListFilesCache] so the root listing — which is otherwise cached for a
/// full day — still refetches when the user actively asks for it.
Future<ListFilesResponse> listFiles( Future<ListFilesResponse> listFiles(
String path, { String path, {
void Function(ListFilesResponse)? onCacheData, void Function(ListFilesResponse)? onCacheData,
void Function(Object)? onError, void Function(Object)? onError,
bool renew = false,
}) => resolveFromCache<ListFilesResponse>( }) => resolveFromCache<ListFilesResponse>(
(onUpdate, onError) => ListFilesCache( (onUpdate, onError) => ListFilesCache(
path: path, path: path,
onUpdate: onUpdate, onUpdate: onUpdate,
onCacheData: onCacheData, onCacheData: onCacheData,
onError: onError, onError: onError,
renew: renew,
), ),
onError: onError, onError: onError,
operationName: 'listFiles', operationName: 'listFiles',