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.
ttl_cache_service #
A high-performance in-memory cache for Dart & Flutter that keeps your expensive queries cached longer and lets you cache any async function with just .cache().
Why ttl_cache_service? #
In traditional LRU caches, all cached data is treated equally:
- A local config read takes ~1ms.
- A heavy database query with joins takes ~1,200ms.
Under classic LRU, your expensive 1.2-second query gets kicked out of memory simply because a cheap 1ms config was read right after it.
ttl_cache_service solves this with Cost-Aware Eviction:
- Automatically tracks how long each query takes to fetch.
- When the cache is full, it evicts the cheapest queries first (the fastest to re-run), keeping your heaviest queries safely in memory.
- Falls back to LRU for ties.
Superpowers #
- ๐ช Just add
.cache(): Call.cache(...)directly on your functions. Keys are auto-generated. - โก Latency-Aware Eviction: Prioritizes keeping high-latency queries in memory.
- ๐ก๏ธ Stampede Protection: If 10 screens request the same data at the same millisecond, only 1 network call runs.
- ๐งน Smart Invalidation: Invalidate by exact parameters, partial prefix, or function name.
- ๐ Quantifiable Latency Saved: See exactly how many milliseconds of network time your cache saved.
- ๐ Zero Dependencies: 100% pure Dart. Works on Flutter (all platforms), Dart VM, Web, and Server.
Installation #
dart pub add ttl_cache_service
# or, for Flutter projects:
flutter pub add ttl_cache_service
Quickstart #
1. Zero-Boilerplate Function Caching (.cache()) #
Call .cache() directly on any asynchronous function (supports 0 to 4 arguments):
// 0 arguments
final profile = await api.getProfile.cache();
// 1 argument
final doctor = await api.getDoctor.cache(doctorId);
// 2 arguments
final appointments = await api.getAppointments.cache(doctorId, date);
// With optional settings:
final slots = await api.getSlots.cache(
doctorId,
date,
ttl: const Duration(minutes: 10), // custom duration for this call
forceRefresh: true, // force re-fetch and update cache
);
2. Manual / Cache-Aside Pattern (getOrFetch) #
If you prefer explicit cache management or need a custom cache instance:
import 'package:ttl_cache_service/ttl_cache_service.dart';
final cache = TtlCache<String, dynamic>(
defaultTtl: const Duration(minutes: 5),
maxEntries: 500,
evictionPolicy: TtlEvictionPolicy.leastFetchTime,
);
// Fetches from network only if missing or expired:
final data = await cache.getOrFetch(
'my_key',
fetcher: () async => await remoteApi.fetchData(),
);
3. Smart Invalidation (.invalidateCache()) #
Invalidate directly on your function without typing manual string keys:
// Invalidate a specific appointment:
api.getAppointments.invalidateCache(doctorId, date);
// Invalidate all dates for a doctor (partial prefix match):
api.getAppointments.invalidateCache(doctorId);
// Invalidate all appointments across all doctors:
api.getAppointments.invalidateAllCache();
// Or invalidate on a TtlCache instance:
cache.invalidate('my_key');
cache.invalidatePrefix('doctor:123:');
Quick Reference: Function Caching Cheat Sheet #
| Your Function | How to Cache | How to Invalidate |
|---|---|---|
getProfile() |
api.getProfile.cache() |
api.getProfile.invalidateCache() |
getDoctor(id) |
api.getDoctor.cache(id) |
api.getDoctor.invalidateCache(id) |
getAppointments(id, date) |
api.getAppointments.cache(id, date) |
api.getAppointments.invalidateCache(id, date) |
getSlots(id, date, dept) |
api.getSlots.cache(id, date, dept) |
api.getSlots.invalidateCache(id, date, dept) |
filter(a, b, c, d) |
api.filter.cache(a, b, c, d) |
api.filter.invalidateCache(...) |
All
.cache(...)calls accept optional settings:{Duration? ttl, bool forceRefresh, String? key, TtlCache? cache}.
Named Parameters #
For functions with named parameters, use getOrFetch:
Future<List<Doctor>> searchDoctors({required String query, String specialty = 'All'}) async { ... }
final doctors = await TtlCache.defaultInstance.getOrFetch(
'searchDoctors:$query:$specialty',
fetcher: () => searchDoctors(query: query, specialty: specialty),
);
// Invalidate when needed:
TtlCache.defaultInstance.invalidate('searchDoctors:$query:$specialty');
Tip: You can also use
TtlCache.buildKey(searchDoctors, [query, specialty])to automatically generate deterministic keys without manual string concatenation.
Stampede / Thundering-Herd Protection #
When 10 parallel requests call for the same key at the same moment, TtlCache executes the network fetch exactly once:
// All 10 callers share the same in-flight Future:
final results = await Future.wait(
List.generate(10, (_) => api.getAppointments.cache(doctorId, date)),
);
Using with Flutter (Cubit example) #
class DoctorSearchCubit extends Cubit<DoctorSearchState> {
DoctorSearchCubit(this._search) : super(DoctorSearchInitial());
final Future<List<Doctor>> Function(String, String) _search;
Future<void> search(String query, String specialty) async {
emit(DoctorSearchLoading());
try {
final doctors = await _search.cache(query, specialty);
emit(DoctorSearchLoaded(doctors));
} catch (e) {
emit(DoctorSearchError(e.toString()));
}
}
}
Cache Stats & Saved Latency #
print(TtlCache.defaultInstance.stats);
// Output:
// TtlCacheStats(hits: 42, misses: 3, hitRate: 93.3%, evictions: 0, size: 3, savedTime: 3200ms)
License #
MIT License. See LICENSE for details.