ttl_cache_service 1.0.0
ttl_cache_service: ^1.0.0 copied to clipboard
High-performance in-memory TTL cache with latency-aware eviction (least fetch time evicted first), stampede protection, and func.getTtlCache() extensions.
example/example.dart
import 'dart:async';
import 'package:ttl_cache_service/ttl_cache_service.dart';
// Simulated remote service
class DoctorApi {
int fetchCount = 0;
Future<Map<String, dynamic>> getDoctor(String id) async {
fetchCount++;
await Future.delayed(const Duration(milliseconds: 50));
return {'id': id, 'name': 'Dr. Ahmed Mahmoud', 'specialty': 'Cardiology'};
}
Future<List<String>> getAppointments(String docId, String date) async {
fetchCount++;
await Future.delayed(const Duration(milliseconds: 100));
return ['10:00 AM', '11:30 AM', '02:00 PM'];
}
}
void main() async {
print('=== 1. Testing func.cache() with Auto Key Generation ===');
final api = DoctorApi();
// Call 1: Cache Miss -> executes network call
final doc1 = await api.getDoctor.cache('doc_1');
print('Call 1 result: $doc1 (Network fetch count: ${api.fetchCount})');
// Call 2: Cache Hit -> returns instantly (0ms), NO network call
final doc2 = await api.getDoctor.cache('doc_1');
print('Call 2 result: $doc2 (Network fetch count: ${api.fetchCount})');
assert(api.fetchCount == 1, 'Network call must not execute on cache hit!');
// Multi-arg call with auto key generation ('getAppointments:doc_1:2026-09-12')
final slots1 = await api.getAppointments.cache('doc_1', '2026-09-12');
print('Slots Call 1: $slots1 (Network fetch count: ${api.fetchCount})');
final slots2 = await api.getAppointments.cache('doc_1', '2026-09-12');
print('Slots Call 2: $slots2 (Network fetch count: ${api.fetchCount})');
assert(api.fetchCount == 2, 'Slots hit must not increment fetch count!');
print('\n=== 2. Testing func.invalidateCache() (Zero Manual Keys) ===');
// Invalidate exact doctor entry without specifying manual key:
final removedCount = api.getDoctor.invalidateCache('doc_1');
print('Invalidated doc_1: $removedCount entry removed');
assert(removedCount == 1, 'doc_1 entry must be invalidated!');
// Calling getDoctor now must re-fetch from network (fetch count becomes 3)
await api.getDoctor.cache('doc_1');
print('After invalidation, fetch count: ${api.fetchCount} (re-fetched as expected)');
assert(api.fetchCount == 3, 'Network fetch must re-run after invalidation!');
// Seed multiple dates for doc_1 and doc_2
await api.getAppointments.cache('doc_1', '2026-09-13');
await api.getAppointments.cache('doc_2', '2026-09-12');
// Partial invalidation: invalidate all dates for doc_1, keeping doc_2 intact
final partialRemoved = api.getAppointments.invalidateCache('doc_1');
print('Partial invalidation for doc_1 removed: $partialRemoved entries');
assert(partialRemoved >= 2, 'Must remove all entries for doc_1');
// Complete function invalidation: invalidate all appointments for all doctors
final allRemoved = api.getAppointments.invalidateAllCache();
print('Complete invalidation of getAppointments removed: $allRemoved entries');
print('\n=== 3. Testing Stampede / Thundering-Herd Protection ===');
var stampedeExecutions = 0;
Future<String> expensiveQuery() async {
stampedeExecutions++;
await Future.delayed(const Duration(milliseconds: 100));
return 'Computation Result';
}
// Launch 10 simultaneous parallel callers for the exact same key
final results = await Future.wait(
List.generate(10, (_) => expensiveQuery.cache()),
);
print('Results count: ${results.length}, Executions: $stampedeExecutions');
assert(stampedeExecutions == 1, 'Stampede protection must coalesce requests into 1 execution!');
print('\n=== 4. Testing Latency-Aware Eviction (Evict Least Fetch Time) ===');
final testCache = TtlCache<String, String>(
maxEntries: 2,
evictionPolicy: TtlEvictionPolicy.leastFetchTime,
);
// Key A: Very expensive query (takes 1000ms)
testCache.set('expensive_query', 'Val A', fetchDuration: const Duration(milliseconds: 1000));
// Key B: Very cheap query (takes 2ms)
testCache.set('cheap_query', 'Val B', fetchDuration: const Duration(milliseconds: 2));
// Cache is now full (2 / 2). Inserting Key C (50ms) triggers capacity eviction:
testCache.set('medium_query', 'Val C', fetchDuration: const Duration(milliseconds: 50));
print('expensive_query cached? ${testCache.has("expensive_query")} (Preserved because it took 1000ms)');
print('cheap_query cached? ${testCache.has("cheap_query")} (Evicted because it was cheapest: 2ms)');
print('medium_query cached? ${testCache.has("medium_query")} (Newly inserted)');
assert(testCache.has('expensive_query'), 'Expensive query must be preserved!');
assert(!testCache.has('cheap_query'), 'Cheapest query must be evicted first!');
print('\n=== 5. Testing Prefix Invalidation on TtlCache ===');
final clinicCache = TtlCache<String, String>();
clinicCache.set('clinic:1:profile', 'Clinic 1');
clinicCache.set('clinic:1:settings', 'Settings 1');
clinicCache.set('clinic:2:profile', 'Clinic 2');
print('Before prefix invalidation size: ${clinicCache.size}');
clinicCache.invalidatePrefix('clinic:1:');
print('After invalidating clinic:1: prefix, size: ${clinicCache.size}');
assert(!clinicCache.has('clinic:1:profile'));
assert(clinicCache.has('clinic:2:profile'));
print('\n=== 6. Cache Statistics & Wall-Clock Latency Saved ===');
print('Global default cache stats: ${TtlCache.defaultInstance.stats}');
print('Test cache stats: ${testCache.stats}');
print('\nAll checks passed successfully! 🎉');
}