Last active
May 24, 2024 16:58
-
-
Save CoderNamedHendrick/dde176c44858b92667dfa6f4cb01fa4e to your computer and use it in GitHub Desktop.
Cache implementation. Provides basic building blocks to implement the popular caching strategies: read-aside, read-through and write-through. Also extendible enough to implement other strategies on.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dart:convert'; | |
final class CacheItem { | |
final String key; | |
final Object data; | |
final DateTime expiry; | |
const CacheItem._(this.key, this.data, this.expiry); | |
/// This factory constructor is used for caching data that shouldn't persist | |
/// so they're set to expired just as they're created by setting the expiry | |
/// to the current time. | |
factory CacheItem.ephemeralStore({ | |
required String key, | |
required Object data, | |
}) { | |
return CacheItem._(key, data, DateTime.now()); | |
} | |
/// This factory constructor is used to create cache items that will persist | |
/// for the specified duration as soon as they're created. | |
factory CacheItem.persistentStore({ | |
required String key, | |
required Object data, | |
required Duration duration, | |
}) { | |
return CacheItem._(key, data, DateTime.now().add(duration)); | |
} | |
bool get isExpired => DateTime.now().isAfter(expiry); | |
bool get isValid => !isExpired; | |
bool get isInvalid => !isValid; | |
String toCacheEntryString() { | |
return jsonEncode({ | |
'expiry': expiry.toIso8601String(), | |
'data': data, | |
}); | |
} | |
static CacheItem fromCacheEntryString(String entry, {required String key}) { | |
final json = jsonDecode(entry); | |
return CacheItem._( | |
key, | |
json['data'], | |
DateTime.parse(json['expiry']), | |
); | |
} | |
CacheItem copyWith({ | |
String? key, | |
Object? data, | |
DateTime? expiry, | |
Duration? persistenceDuration, | |
}) { | |
return CacheItem._( | |
key ?? this.key, | |
data ?? this.data, | |
switch ((expiry, persistenceDuration)) { | |
(final expiry?, final persistenceDuration?) => | |
expiry.add(persistenceDuration), | |
(final expiry?, null) => expiry, | |
(null, final persistenceDuration?) => | |
DateTime.now().add(persistenceDuration), | |
(null, null) => this.expiry, | |
}, | |
); | |
} | |
@override | |
bool operator ==(Object other) { | |
if (identical(this, other)) return true; | |
return other is CacheItem && | |
other.key == key && | |
other.data == data && | |
other.expiry == expiry; | |
} | |
@override | |
int get hashCode => key.hashCode ^ data.hashCode ^ expiry.hashCode; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
abstract interface class CacheStore { | |
Future<int> get cacheVersion; | |
Future<void> updateCacheVersion(int version); | |
Future<void> initialiseStore(); | |
Future<void> saveCacheItem(CacheItem item); | |
Future<CacheItem?> getCacheItem(String key); | |
Future<void> invalidateCacheItem(String key); | |
Future<void> invalidateCache(); | |
bool containsKey(String key); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:test/expect.dart'; | |
import 'package:test/scaffolding.dart'; | |
import '../bin/cache_manager/cache_manager.dart'; | |
void main() { | |
group('Cache test suite', () { | |
late CacheManager cache; | |
setUp(() { | |
CacheManager.init(store: InMemoryCacheStore()); | |
cache = CacheManager.instance; | |
}); | |
test('ephemeral cache items expire immediately', () async { | |
final item = CacheItem.ephemeralStore( | |
key: 'test-key-1', | |
data: {'name': 'John Doe'}, | |
); | |
cache.set(item); | |
final cachedItem = await cache.get('test-key-1'); | |
expect(cachedItem?.isExpired, true); | |
expect(cachedItem?.data, {'name': 'John Doe'}); | |
}); | |
test('persistent cache items last as long as their specified duration', | |
() async { | |
final item = CacheItem.persistentStore( | |
key: 'test-key-2', | |
data: {'message': 'Hello world'}, | |
duration: Duration(seconds: 3), | |
); | |
cache.set(item); | |
final cachedItem = await cache.get('test-key-2'); | |
expect(cachedItem?.isExpired, false); | |
expect(cachedItem?.data, {'message': 'Hello world'}); | |
await Future.delayed(Duration(seconds: 3)); | |
expect(cachedItem?.isExpired, true); | |
expect(cachedItem?.data, {'message': 'Hello world'}); | |
}); | |
test('override expiry on persistent cache item', () async { | |
final item = CacheItem.persistentStore( | |
key: 'test-key-3', | |
data: {'data': 'look at me'}, | |
duration: Duration(seconds: 40), | |
); | |
cache.set(item); | |
final cachedItem = await cache.get('test-key-3'); | |
expect(cachedItem?.isExpired, false); | |
expect(cachedItem?.data, {'data': 'look at me'}); | |
cache.set(item.copyWith(persistenceDuration: Duration.zero)); | |
final updatedCachedItem = await cache.get('test-key-3'); | |
expect(updatedCachedItem?.isExpired, true); | |
expect(updatedCachedItem?.data, {'data': 'look at me'}); | |
}); | |
}); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:hive_flutter/hive_flutter.dart'; | |
import 'cache_item.dart'; | |
import 'cache_store.dart'; | |
// For flutter apps replace, hive with hive | |
final class HiveCacheStore implements CacheStore { | |
late final Box<String> _store; | |
@override | |
Future<int> get cacheVersion async { | |
final version = _store.get('version'); | |
if (version == null) return -1; | |
return int.parse(version); | |
} | |
@override | |
Future<void> updateCacheVersion(int version) async { | |
return await _store.put('version', version.toString()); | |
} | |
@override | |
bool containsKey(String key) { | |
return _store.containsKey(key); | |
} | |
@override | |
Future<CacheItem?> getCacheItem(String key) async { | |
final item = _store.get(key); | |
if (item == null) return null; | |
return CacheItem.fromCacheEntryString(item, key: key); | |
} | |
@override | |
Future<void> initialiseStore() async { | |
await Hive.initFlutter(); | |
_store = await Hive.openBox('cache-store'); | |
} | |
@override | |
Future<void> invalidateCache() async { | |
await _store.clear(); | |
} | |
@override | |
Future<void> invalidateCacheItem(String key) async { | |
final item = await getCacheItem(key); | |
if (item == null) return; | |
return await saveCacheItem( | |
item.copyWith(persistenceDuration: const Duration(minutes: -5))); | |
} | |
@override | |
Future<void> saveCacheItem(CacheItem item) async { | |
return await _store.put(item.key, item.toCacheEntryString()); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'cache_store.dart'; | |
import 'cache_item.dart'; | |
final class InMemoryCacheStore implements CacheStore { | |
InMemoryCacheStore(); | |
late final Map<String, String> _store; | |
@override | |
Future<int> get cacheVersion async { | |
return int.parse(_store['version'] ?? '-1'); | |
} | |
@override | |
Future<void> updateCacheVersion(int version) async { | |
_store['version'] = version.toString(); | |
} | |
@override | |
bool containsKey(String key) { | |
return _store.containsKey(key); | |
} | |
@override | |
Future<CacheItem?> getCacheItem(String key) async { | |
final item = _store[key]; | |
if (item == null) return null; | |
return CacheItem.fromCacheEntryString(item, key: key); | |
} | |
@override | |
Future<void> initialiseStore() async { | |
_store = {}; | |
} | |
@override | |
Future<void> invalidateCache() async { | |
return _store.clear(); | |
} | |
@override | |
Future<void> invalidateCacheItem(String key) async { | |
final item = await getCacheItem(key); | |
if (item == null) return; | |
return await saveCacheItem( | |
item.copyWith(persistenceDuration: const Duration(minutes: -5))); | |
} | |
@override | |
Future<void> saveCacheItem(CacheItem item) async { | |
_store[item.key] = item.toCacheEntryString(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'cache_item.dart'; | |
import 'cache_store.dart'; | |
inal class CacheManager { | |
late final CacheStore _store; | |
CacheManager._(); | |
static CacheManager? _instance; | |
static CacheManager get instance { | |
if (_instance == null) { | |
throw ArgumentError('Cache not initialized'); | |
} | |
return _instance!; | |
} | |
static Future<void> init({required CacheStore store}) async { | |
_instance = CacheManager._(); | |
_instance!._store = store; | |
await instance._store.initialiseStore(); | |
} | |
Future<int> cacheVersion() async { | |
return await _store.cacheVersion; | |
} | |
Future<void> updateCacheVersion(int version) async { | |
return await _store.updateCacheVersion(version); | |
} | |
Future<void> set(CacheItem item) async { | |
return await _store.saveCacheItem(item); | |
} | |
Future<CacheItem?> get(String key) async { | |
return await _store.getCacheItem(key); | |
} | |
bool contains(String key) { | |
return _store.containsKey(key); | |
} | |
Future<void> invalidateCacheItem(String key) async { | |
return await _store.invalidateCacheItem(key); | |
} | |
Future<bool> cacheItemExpired(String key) async { | |
final item = await get(key); | |
return item?.isExpired ?? true; | |
} | |
Future<void> invalidateCache() async { | |
return await _store.invalidateCache(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
final class CacheManagerUtils { | |
static String composeKeyFromUrl( | |
String path, { | |
required String requestMethod, | |
Map<String, dynamic>? queryParams, | |
}) { | |
return '$requestMethod:$path${queryParams?.queryString ?? ''}'; | |
} | |
} | |
extension on Map<String, dynamic> { | |
String get queryString { | |
final buffer = StringBuffer(); | |
for (int i = 0; i < length; i++) { | |
if (i > 0) buffer.write('&'); | |
final entry = entries.elementAt(i); | |
buffer.write('${entry.key}=${entry.value}'); | |
} | |
return '?${buffer.toString()}'; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment