feat(cache): Add lightweight in-memory cache with TTL and LRU eviction

This commit is contained in:
cogwheel0
2025-11-22 21:53:14 +05:30
parent 8ed75f8f14
commit c4a36bb51c
14 changed files with 1298 additions and 242 deletions

View File

@@ -5,9 +5,11 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
// Types are used through app_providers.dart
import '../providers/app_providers.dart';
import '../models/user.dart';
import '../services/optimized_storage_service.dart';
import 'token_validator.dart';
import 'auth_cache_manager.dart';
import '../utils/debug_logger.dart';
import '../utils/user_avatar_utils.dart';
part 'auth_state_manager.g.dart';
@@ -97,12 +99,63 @@ class AuthStateManager extends _$AuthStateManager {
state.asData?.value ?? const AuthState(status: AuthStatus.initial);
void _set(AuthState next, {bool cache = false}) {
final storage = ref.read(optimizedStorageServiceProvider);
if (next.user != null && next.isAuthenticated) {
// Persist user and avatar asynchronously without blocking state update
unawaited(_persistUserWithAvatar(next, storage));
} else if (!next.isAuthenticated) {
unawaited(storage.saveLocalUser(null).onError((error, stack) {
DebugLogger.error(
'Failed to clear local user on logout',
scope: 'auth/persistence',
error: error,
stackTrace: stack,
);
}));
unawaited(storage.saveLocalUserAvatar(null).onError((error, stack) {
DebugLogger.error(
'Failed to clear local user avatar on logout',
scope: 'auth/persistence',
error: error,
stackTrace: stack,
);
}));
}
state = AsyncValue.data(next);
if (cache) {
_cacheManager.cacheAuthState(next);
}
}
Future<void> _persistUserWithAvatar(
AuthState authState,
OptimizedStorageService storage,
) async {
try {
final api = ref.read(apiServiceProvider);
final user = authState.user!;
final resolvedAvatar = resolveUserProfileImageUrl(
api,
deriveUserProfileImage(user),
);
final userWithAvatar =
resolvedAvatar != null && resolvedAvatar != user.profileImage
? user.copyWith(profileImage: resolvedAvatar)
: user;
await storage.saveLocalUser(userWithAvatar);
if (resolvedAvatar != null) {
await storage.saveLocalUserAvatar(resolvedAvatar);
}
} catch (error, stack) {
DebugLogger.error(
'Failed to persist user with avatar',
scope: 'auth/persistence',
error: error,
stackTrace: stack,
);
}
}
void _update(
AuthState Function(AuthState current) transform, {
bool cache = false,
@@ -143,6 +196,25 @@ class AuthStateManager extends _$AuthStateManager {
cache: true,
);
try {
final cachedUser = await storage.getLocalUser();
if (cachedUser != null) {
// Restore cached avatar as well
final cachedAvatar = await storage.getLocalUserAvatar();
final userWithAvatar =
cachedAvatar != null &&
cachedAvatar.isNotEmpty &&
cachedUser.profileImage != cachedAvatar
? cachedUser.copyWith(profileImage: cachedAvatar)
: cachedUser;
_update(
(current) => current.copyWith(user: userWithAvatar),
cache: true,
);
DebugLogger.auth('Restored user from cache');
}
} catch (_) {}
// Update API service with token and kick off dependent background work
_updateApiServiceToken(token);
_preloadDefaultModel();
@@ -706,11 +778,11 @@ class AuthStateManager extends _$AuthStateManager {
// Clear active server to force return to server connection page
await storage.setActiveServerId(null);
// Invalidate all auth-related providers to clear cached data
ref.invalidate(activeServerProvider);
ref.invalidate(serverConfigsProvider);
// Clear auth cache manager
_cacheManager.clearAuthCache();
@@ -725,7 +797,9 @@ class AuthStateManager extends _$AuthStateManager {
),
);
DebugLogger.auth('Logout complete - all data cleared including server configs and custom headers');
DebugLogger.auth(
'Logout complete - all data cleared including server configs and custom headers',
);
} catch (e, stack) {
DebugLogger.error(
'logout-failed',