1import 'dart:collection'; 2 3import 'package:flutter/foundation.dart'; 4import '../providers/auth_provider.dart'; 5import '../providers/comments_provider.dart'; 6import '../providers/vote_provider.dart'; 7import 'comment_service.dart'; 8 9/// Comments Provider Cache 10/// 11/// Manages cached CommentsProvider instances per post URI using LRU eviction. 12/// Inspired by Thunder app's architecture for instant back navigation. 13/// 14/// Key features: 15/// - One CommentsProvider per post URI 16/// - LRU eviction (default: 15 most recent posts) 17/// - Sign-out cleanup via AuthProvider listener 18/// 19/// Usage: 20/// ```dart 21/// final cache = context.read<CommentsProviderCache>(); 22/// final provider = cache.getProvider( 23/// postUri: post.uri, 24/// postCid: post.cid, 25/// ); 26/// ``` 27class CommentsProviderCache { 28 CommentsProviderCache({ 29 required AuthProvider authProvider, 30 required VoteProvider voteProvider, 31 required CommentService commentService, 32 this.maxSize = 15, 33 }) : _authProvider = authProvider, 34 _voteProvider = voteProvider, 35 _commentService = commentService { 36 _wasAuthenticated = _authProvider.isAuthenticated; 37 _authProvider.addListener(_onAuthChanged); 38 } 39 40 final AuthProvider _authProvider; 41 final VoteProvider _voteProvider; 42 final CommentService _commentService; 43 44 /// Maximum number of providers to cache 45 final int maxSize; 46 47 /// LRU cache - LinkedHashMap maintains insertion order 48 /// Most recently accessed items are at the end 49 final LinkedHashMap<String, CommentsProvider> _cache = LinkedHashMap(); 50 51 /// Reference counts for "in-use" providers. 52 /// 53 /// Screens that hold onto a provider instance should call [acquireProvider] 54 /// and later [releaseProvider] to prevent LRU eviction from disposing a 55 /// provider that is still mounted in the navigation stack. 56 final Map<String, int> _refCounts = {}; 57 58 /// Track auth state for sign-out detection 59 bool _wasAuthenticated = false; 60 61 /// Acquire (get or create) a CommentsProvider for a post. 62 /// 63 /// This "pins" the provider to avoid LRU eviction while in use. 64 /// Call [releaseProvider] when the consumer unmounts. 65 /// 66 /// If provider exists in cache, moves it to end (LRU touch). 67 /// If cache is full, evicts the oldest *unreferenced* provider before 68 /// creating a new one. If all providers are currently referenced, the cache 69 /// may temporarily exceed [maxSize] to avoid disposing active providers. 70 CommentsProvider acquireProvider({ 71 required String postUri, 72 required String postCid, 73 }) { 74 final provider = _getOrCreateProvider(postUri: postUri, postCid: postCid); 75 _refCounts[postUri] = (_refCounts[postUri] ?? 0) + 1; 76 return provider; 77 } 78 79 /// Release a previously acquired provider for a post. 80 /// 81 /// Once released, the provider becomes eligible for LRU eviction. 82 void releaseProvider(String postUri) { 83 final current = _refCounts[postUri]; 84 if (current == null) { 85 return; 86 } 87 88 if (current <= 1) { 89 _refCounts.remove(postUri); 90 } else { 91 _refCounts[postUri] = current - 1; 92 } 93 94 _evictIfNeeded(); 95 } 96 97 /// Legacy name kept for compatibility: prefer [acquireProvider]. 98 CommentsProvider getProvider({ 99 required String postUri, 100 required String postCid, 101 }) => acquireProvider(postUri: postUri, postCid: postCid); 102 103 CommentsProvider _getOrCreateProvider({ 104 required String postUri, 105 required String postCid, 106 }) { 107 // Check if already cached 108 if (_cache.containsKey(postUri)) { 109 // Move to end (most recently used) 110 final provider = _cache.remove(postUri)!; 111 _cache[postUri] = provider; 112 113 if (kDebugMode) { 114 debugPrint('📦 Cache hit: $postUri (${_cache.length}/$maxSize)'); 115 } 116 117 return provider; 118 } 119 120 // Evict unreferenced providers if at capacity. 121 if (_cache.length >= maxSize) { 122 _evictIfNeeded(includingOne: true); 123 } 124 125 // Create new provider 126 final provider = CommentsProvider( 127 _authProvider, 128 voteProvider: _voteProvider, 129 commentService: _commentService, 130 postUri: postUri, 131 postCid: postCid, 132 ); 133 134 _cache[postUri] = provider; 135 136 if (kDebugMode) { 137 debugPrint('📦 Cache miss: $postUri (${_cache.length}/$maxSize)'); 138 if (_cache.length > maxSize) { 139 debugPrint( 140 '📌 Cache exceeded maxSize because active providers are pinned', 141 ); 142 } 143 } 144 145 return provider; 146 } 147 148 void _evictIfNeeded({bool includingOne = false}) { 149 final targetSize = includingOne ? maxSize - 1 : maxSize; 150 while (_cache.length > targetSize) { 151 String? oldestUnreferencedKey; 152 for (final key in _cache.keys) { 153 if ((_refCounts[key] ?? 0) == 0) { 154 oldestUnreferencedKey = key; 155 break; 156 } 157 } 158 159 if (oldestUnreferencedKey == null) { 160 break; 161 } 162 163 final evicted = _cache.remove(oldestUnreferencedKey); 164 evicted?.dispose(); 165 166 if (kDebugMode) { 167 debugPrint('🗑️ Cache evict: $oldestUnreferencedKey'); 168 } 169 } 170 } 171 172 /// Check if provider exists without creating 173 bool hasProvider(String postUri) => _cache.containsKey(postUri); 174 175 /// Get existing provider without creating (for checking state) 176 CommentsProvider? peekProvider(String postUri) => _cache[postUri]; 177 178 /// Remove specific provider (e.g., after post deletion) 179 void removeProvider(String postUri) { 180 final provider = _cache.remove(postUri); 181 _refCounts.remove(postUri); 182 provider?.dispose(); 183 } 184 185 /// Handle auth state changes - clear all on sign-out 186 void _onAuthChanged() { 187 final isAuthenticated = _authProvider.isAuthenticated; 188 189 // Clear all cached providers on sign-out 190 if (_wasAuthenticated && !isAuthenticated) { 191 if (kDebugMode) { 192 debugPrint('🔒 User signed out - clearing ${_cache.length} cached comment providers'); 193 } 194 clearAll(); 195 } 196 197 _wasAuthenticated = isAuthenticated; 198 } 199 200 /// Clear all cached providers 201 void clearAll() { 202 for (final provider in _cache.values) { 203 provider.dispose(); 204 } 205 _cache.clear(); 206 _refCounts.clear(); 207 } 208 209 /// Current cache size 210 int get size => _cache.length; 211 212 /// Dispose and cleanup 213 void dispose() { 214 _authProvider.removeListener(_onAuthChanged); 215 clearAll(); 216 } 217}