Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 1.1.0

- changed method signature from `hasCacheItemExpired` to `isCacheItemExpired`
- expose a where API which takes a key and a predicate for more conditional CacheStore filtering

## 1.0.2

- moved cache utils to its own package
Expand Down
3 changes: 1 addition & 2 deletions lib/cache_manager_plus.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
library;

export 'src/cache/cache.dart';
export 'src/in_memory_store.dart';
export 'src/src.dart';
4 changes: 2 additions & 2 deletions lib/src/cache/cache_item.dart → lib/src/cache_item.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@ final class CacheItem {

bool get isValid => !isExpired;

bool get isInvalid => !isValid;

String toCacheEntryString() {
return jsonEncode({
'expiry': expiry.toIso8601String(),
Expand Down Expand Up @@ -79,6 +77,8 @@ final class CacheItem {
other.expiry == expiry;
}

//coverage:ignore-start
@override
int get hashCode => key.hashCode ^ data.hashCode ^ expiry.hashCode;
//coverage:ignore-end
}
8 changes: 8 additions & 0 deletions lib/src/cache/cache_store.dart → lib/src/cache_store.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//coverage:ignore-file

import 'dart:async';

import '../../cache_manager_plus.dart';
Expand Down Expand Up @@ -79,4 +81,10 @@ abstract interface class CacheStore {
///
/// ```
bool containsKey(String key);

/// contract requirement for closing the cache store
/// helpful to allow stores be closed safely to prevent memory leaks.
/// When implemented, the client should have to reinitialise the store to read
/// its content
FutureOr<void> close();
}
51 changes: 42 additions & 9 deletions lib/src/in_memory_store.dart
Original file line number Diff line number Diff line change
@@ -1,41 +1,69 @@
import 'cache/cache.dart';
import 'dart:async';

import 'cache_item.dart';
import 'cache_store.dart';

final class InMemoryCacheStore implements CacheStore {
InMemoryCacheStore();

late final Map<String, String> _store;
Map<String, String>? _store;

@override
Future<int> get cacheVersion async {
return int.parse(_store['version'] ?? '-1');
if (_store == null) {
throw StateError(

Check warning on line 14 in lib/src/in_memory_store.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/in_memory_store.dart#L14

Added line #L14 was not covered by tests
'Store not initialised, did you fail to initialise the store?');
}

return int.parse(_store!['version'] ?? '-1');
}

@override
Future<void> updateCacheVersion(int version) async {
_store['version'] = version.toString();
if (_store == null) {
throw StateError(

Check warning on line 24 in lib/src/in_memory_store.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/in_memory_store.dart#L24

Added line #L24 was not covered by tests
'Store not initialised, did you fail to initialise the store?');
}

_store!['version'] = version.toString();
}

@override
bool containsKey(String key) {
return _store.containsKey(key);
if (_store == null) {
throw StateError(

Check warning on line 34 in lib/src/in_memory_store.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/in_memory_store.dart#L34

Added line #L34 was not covered by tests
'Store not initialised, did you fail to initialise the store?');
}

return _store!.containsKey(key);
}

@override
Future<CacheItem?> getCacheItem(String key) async {
final item = _store[key];
if (_store == null) {
throw StateError(

Check warning on line 44 in lib/src/in_memory_store.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/in_memory_store.dart#L44

Added line #L44 was not covered by tests
'Store not initialised, did you fail to initialise the store?');
}

final item = _store![key];
if (item == null) return null;

return CacheItem.fromCacheEntryString(item, key: key);
}

@override
Future<void> initialiseStore() async {
_store = {};
_store ??= {};
}

@override
Future<void> invalidateCache() async {
return _store.clear();
if (_store == null) {
throw StateError(

Check warning on line 62 in lib/src/in_memory_store.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/in_memory_store.dart#L62

Added line #L62 was not covered by tests
'Store not initialised, did you fail to initialise the store?');
}

return _store!.clear();
}

@override
Expand All @@ -49,6 +77,11 @@

@override
Future<void> saveCacheItem(CacheItem item) async {
_store[item.key] = item.toCacheEntryString();
_store![item.key] = item.toCacheEntryString();
}

@override
FutureOr<void> close() {
_store = null;
}
}
42 changes: 31 additions & 11 deletions lib/src/cache/manager.dart → lib/src/manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,12 @@ final class CacheManager {
List<CacheStore> stores = const [],
bool forceInit = false,
}) async {
assert(store != null || stores.isNotEmpty,
'At least one store must be provided');

if (store == null && stores.isEmpty) {
throw ArgumentError('At least one store must be provided');
}

if (forceInit) {
await close(); // safe closes all stores
_instance = CacheManager._();
} else {
if (_instance != null) {
Expand All @@ -57,18 +55,40 @@ final class CacheManager {
_instance ??= CacheManager._();
}

_instance!._stores = {
if (store != null) store.runtimeType.toString(): store,
for (final store in stores) store.runtimeType.toString(): store,
};
_instance!._stores = {};
if (store case final CacheStore s) {
_instance!._stores.putIfAbsent(s.runtimeType.toString(), () => s);
}
for (final store in stores) {
_instance!._stores.putIfAbsent(store.runtimeType.toString(), () => store);
}

await Future.wait(
instance._stores.values.map((store) => store.initialiseStore()),
);
}

static void close() {
_instance = null;
/// closes all stores
static Future<void> close() async {
if (_instance != null) {
await _instance!._stores.values
.map((store) async => await store.close())
.wait;
_instance = null;
}
}

/// retrieves all [CacheStore] that matches the [predicate]
Future<Iterable<CacheStore>> where(
String key, bool Function(CacheItem i) predicate) async {
List<CacheStore> s = [];
for (final store in _stores.values) {
if (await store.getCacheItem(key) case final CacheItem it) {
if (predicate(it)) s.add(store);
}
}

return s;
}

/// retrieves the [CacheStore] version for the [CacheStore] specified by
Expand Down Expand Up @@ -163,11 +183,11 @@ final class CacheManager {
return await invalidateItemInCache(_effectiveStore(S.toString()));
}

/// helper method to check if a [CacheItem] has expired/valid. It returns a
/// helper method to check if a [CacheItem] is expired/valid. It returns a
/// nullable boolean with null being returned when the item doesn't exist
/// in the [CacheStore] specified by the method's generic type [S]
/// or the first [CacheStore] in the manager
FutureOr<bool?> hasCacheItemExpired<S extends CacheStore>(String key) async {
FutureOr<bool?> isCacheItemExpired<S extends CacheStore>(String key) async {
final item = await _effectiveStore(S.toString()).getCacheItem(key);
return item?.isExpired;
}
Expand Down
1 change: 1 addition & 0 deletions lib/src/cache/cache.dart → lib/src/src.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
//GENERATED BARREL FILE
export 'cache_item.dart';
export 'cache_store.dart';
export 'in_memory_store.dart';
export 'manager.dart';
4 changes: 1 addition & 3 deletions pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: cache_manager_plus
description: "A Dart package for managing cache with support for multiple storage backends."
version: 1.0.2
version: 1.1.0
homepage: https://github.com/CoderNamedHendrick/cache_manager
repository: https://github.com/CoderNamedHendrick/cache_manager
issue_tracker: https://github.com/CoderNamedHendrick/cache_manager/issues
Expand All @@ -15,8 +15,6 @@ topics:
environment:
sdk: ^3.0.0

dependencies:

dev_dependencies:
lints: any
test: ^1.25.15
35 changes: 35 additions & 0 deletions test/cache_item_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import 'package:cache_manager_plus/cache_manager_plus.dart';
import 'package:test/expect.dart';
import 'package:test/scaffolding.dart';

void main() {
group('Cache item test', () {
test('equality comparison', () {
final a = CacheItem.ephemeral(key: 'a', data: 'Data a')
.copyWith(expiry: DateTime(1900));
final b = CacheItem.ephemeral(key: 'a', data: 'Data a')
.copyWith(expiry: DateTime(1900));

expect(a == b, true);
});

test('persistence duration update', () {
var a = CacheItem.ephemeral(key: 'a', data: 'data a');

expect(a.isExpired, true);

a = a.copyWith(persistenceDuration: Duration(minutes: 20));
expect(a.isExpired, false);

a = a.copyWith(
expiry: DateTime.now().add(Duration(minutes: 10)),
persistenceDuration: Duration(minutes: -20));
expect(a.isExpired, true);
final expiry = a.expiry;

a = a.copyWith();

expect(a.expiry, expiry);
});
});
}
63 changes: 61 additions & 2 deletions test/cache_manager_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,27 @@ import 'package:test/test.dart';
void main() {
group('Cache test suite', () {
late CacheManager manager;
setUp(() {
CacheManager.init(store: InMemoryCacheStore(), forceInit: true);
setUp(() async {
await CacheManager.init(store: InMemoryCacheStore(), forceInit: true);
manager = CacheManager.instance;
});

tearDown(() async {
await CacheManager.close();
});

test('not passing any store throws error', () async {
await CacheManager.close();

expect(
CacheManager.init(),
throwsA(
isArgumentError.having((p0) => p0.message, 'invalid arguments error',
'At least one store must be provided'),
),
);
});

test('re-initialisation without forcing throw error', () async {
expect(
CacheManager.init(store: InMemoryCacheStore()),
Expand All @@ -22,6 +38,14 @@ void main() {
);
});

test('update cache version', () async {
expect(await manager.cacheVersion(), -1);

manager.updateCacheVersion(1);

expect(await manager.cacheVersion(), 1);
});

test('ephemeral cache items expire immediately', () async {
final item = CacheItem.ephemeral(
key: 'test-key-1',
Expand Down Expand Up @@ -74,5 +98,40 @@ void main() {
expect(updatedCachedItem?.isExpired, true);
expect(updatedCachedItem?.data, {'data': 'look at me'});
});

test('close store and reinitialise', () async {
final item = CacheItem.persistent(
key: 'test-key-3',
data: {'data': 'look at me'},
duration: Duration(seconds: 40),
);

CacheManager.instance.set(item);

var cachedItem = await CacheManager.instance.get('test-key-3');
expect(cachedItem?.isExpired, false);

await CacheManager.close();

expect(
() async {
await CacheManager.instance.set(item);
}(),
throwsA(
isArgumentError.having(
(p0) => p0.message,
'state error message',
'CacheManager not initialized',
),
),
);

CacheManager.init(store: InMemoryCacheStore());

CacheManager.instance.set(item);

cachedItem = await CacheManager.instance.get('test-key-3');
expect(cachedItem?.isExpired, false);
});
});
}
Loading