diff --git a/apps/lib/core/cache/cache_store.dart b/apps/lib/core/cache/cache_store.dart new file mode 100644 index 0000000..6b4f768 --- /dev/null +++ b/apps/lib/core/cache/cache_store.dart @@ -0,0 +1,5 @@ +abstract class CacheStore { + Future read(String key); + Future write(String key, T value); + Future remove(String key); +} diff --git a/apps/lib/core/cache/hybrid_cache_store.dart b/apps/lib/core/cache/hybrid_cache_store.dart new file mode 100644 index 0000000..a148fcc --- /dev/null +++ b/apps/lib/core/cache/hybrid_cache_store.dart @@ -0,0 +1,55 @@ +import 'memory_cache_store.dart'; +import 'persistent_cache_store.dart'; + +class HybridCacheStore { + final MemoryCacheStore memory; + final PersistentCacheStore persistent; + final Map> _inflight = >{}; + + HybridCacheStore({required this.memory, required this.persistent}); + + Future read(String key) async { + final memoryValue = await memory.read(key); + if (memoryValue != null) { + return memoryValue; + } + final persistentValue = await persistent.read(key); + if (persistentValue != null) { + await memory.write(key, persistentValue); + } + return persistentValue; + } + + Future write(String key, T value) async { + await memory.write(key, value); + await persistent.write(key, value); + } + + Future remove(String key) async { + await memory.remove(key); + await persistent.remove(key); + } + + Future getOrLoad(String key, {required Future Function() loader}) { + final running = _inflight[key]; + if (running != null) { + return running.then((value) => value as T); + } + + final future = () async { + final cached = await read(key); + if (cached != null) { + return cached; + } + + final loaded = await loader(); + await write(key, loaded); + return loaded; + }(); + + _inflight[key] = future; + return future.whenComplete(() { + _inflight.remove(key); + }); + } +} diff --git a/apps/lib/core/cache/memory_cache_store.dart b/apps/lib/core/cache/memory_cache_store.dart new file mode 100644 index 0000000..d91f467 --- /dev/null +++ b/apps/lib/core/cache/memory_cache_store.dart @@ -0,0 +1,24 @@ +import 'cache_store.dart'; + +class MemoryCacheStore implements CacheStore { + final Map _values = {}; + + @override + Future read(String key) async { + final value = _values[key]; + if (value is T) { + return value; + } + return null; + } + + @override + Future write(String key, T value) async { + _values[key] = value; + } + + @override + Future remove(String key) async { + _values.remove(key); + } +} diff --git a/apps/lib/core/cache/persistent_cache_store.dart b/apps/lib/core/cache/persistent_cache_store.dart new file mode 100644 index 0000000..160a7f9 --- /dev/null +++ b/apps/lib/core/cache/persistent_cache_store.dart @@ -0,0 +1,24 @@ +import 'cache_store.dart'; + +class PersistentCacheStore implements CacheStore { + final Map _values = {}; + + @override + Future read(String key) async { + final value = _values[key]; + if (value is T) { + return value; + } + return null; + } + + @override + Future write(String key, T value) async { + _values[key] = value; + } + + @override + Future remove(String key) async { + _values.remove(key); + } +} diff --git a/apps/test/core/cache/hybrid_cache_store_test.dart b/apps/test/core/cache/hybrid_cache_store_test.dart new file mode 100644 index 0000000..770a6e5 --- /dev/null +++ b/apps/test/core/cache/hybrid_cache_store_test.dart @@ -0,0 +1,27 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:social_app/core/cache/hybrid_cache_store.dart'; +import 'package:social_app/core/cache/memory_cache_store.dart'; +import 'package:social_app/core/cache/persistent_cache_store.dart'; + +void main() { + test('same key concurrent load should execute loader once', () async { + var calls = 0; + final store = HybridCacheStore( + memory: MemoryCacheStore(), + persistent: PersistentCacheStore(), + ); + + Future loader() async { + calls += 1; + await Future.delayed(const Duration(milliseconds: 20)); + return 'ok'; + } + + await Future.wait([ + store.getOrLoad('k', loader: loader), + store.getOrLoad('k', loader: loader), + ]); + + expect(calls, 1); + }); +}