Main coves client
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}