From bf28a678c92131eeddd6c62714599018bb39caf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Sat, 9 May 2026 23:39:06 +0200 Subject: [PATCH] implemented background prefetching for files root, added 24-hour caching for root directory listing, and enabled cache renewal for manual refreshes --- .../queries/list_files/list_files_cache.dart | 41 ++++++++++++++++++- lib/main.dart | 15 +++++++ .../app/modules/files/bloc/files_bloc.dart | 7 +++- .../data_provider/files_data_provider.dart | 7 ++++ 4 files changed, 67 insertions(+), 3 deletions(-) diff --git a/lib/api/marianumcloud/webdav/queries/list_files/list_files_cache.dart b/lib/api/marianumcloud/webdav/queries/list_files/list_files_cache.dart index b9a4cd1..878f6a2 100644 --- a/lib/api/marianumcloud/webdav/queries/list_files/list_files_cache.dart +++ b/lib/api/marianumcloud/webdav/queries/list_files/list_files_cache.dart @@ -16,8 +16,9 @@ class ListFilesCache extends SimpleCache { super.onNetworkData, super.onError, required String path, + super.renew = false, }) : super( - cacheTime: RequestCache.cacheNothing, + cacheTime: _cacheTimeFor(path), loader: () => ListFiles(ListFilesParams(path)).run(), fromJson: ListFilesResponse.fromJson, onUpdate: onUpdate, @@ -25,6 +26,44 @@ class ListFilesCache extends SimpleCache { 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 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) { final cacheName = md5 .convert(utf8.encode('MarianumMobile-$path')) diff --git a/lib/main.dart b/lib/main.dart index ad7199e..f003df9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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: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 'app.dart'; import 'background/widget_background_task.dart'; @@ -91,6 +92,20 @@ Future 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) { ErrorWidget.builder = (error) => Material( color: Colors.white, diff --git a/lib/state/app/modules/files/bloc/files_bloc.dart b/lib/state/app/modules/files/bloc/files_bloc.dart index e8753c9..c7e41fd 100644 --- a/lib/state/app/modules/files/bloc/files_bloc.dart +++ b/lib/state/app/modules/files/bloc/files_bloc.dart @@ -37,7 +37,9 @@ class FilesBloc Future refresh() async { add(RefetchStarted()); 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 setPath(List path) async { @@ -52,7 +54,7 @@ class FilesBloc await refresh(); } - Future _query(List path) async { + Future _query(List path, {bool renew = false}) async { final pathString = path.isEmpty ? '/' : path.join('/'); // Drop late results when [setPath] has navigated elsewhere or when the @@ -71,6 +73,7 @@ class FilesBloc try { listing = await repo.data.listFiles( pathString, + renew: renew, onCacheData: (cached) { if (isStale()) return; // Cached payload arrives before the network call settles. Surface it diff --git a/lib/state/app/modules/files/data_provider/files_data_provider.dart b/lib/state/app/modules/files/data_provider/files_data_provider.dart index e721fbb..3d76400 100644 --- a/lib/state/app/modules/files/data_provider/files_data_provider.dart +++ b/lib/state/app/modules/files/data_provider/files_data_provider.dart @@ -11,16 +11,23 @@ class FilesDataProvider { /// network call is still pending. The Future itself resolves once both the /// cache lookup and the network attempt have settled, throwing if no payload /// 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 listFiles( String path, { void Function(ListFilesResponse)? onCacheData, void Function(Object)? onError, + bool renew = false, }) => resolveFromCache( (onUpdate, onError) => ListFilesCache( path: path, onUpdate: onUpdate, onCacheData: onCacheData, onError: onError, + renew: renew, ), onError: onError, operationName: 'listFiles',