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:
@@ -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'))
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
Reference in New Issue
Block a user