Skip to content

Commit d4b5840

Browse files
authored
feat: 'JsonCacheException' and 'JsonCacheTry' (#101)
1 parent 81e0162 commit d4b5840

11 files changed

+346
-61
lines changed

CHANGELOG.md

+14
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- JsonCacheTry: an implementation that throws a `JsonCacheException` when a
13+
cache failure occurs —
14+
[100](https://github.com/dartoos-dev/json_cache/issues/100).
15+
16+
- JsonCacheException: an exception that conveys enhanced diagnostic messages
17+
about cache operation failures —
18+
[100](https://github.com/dartoos-dev/json_cache/issues/100).
19+
20+
### Changed
21+
22+
- Improvements to the README file.
23+
1024
## [1.3.1] - 2022-08-25
1125

1226
### Changed

README.md

+29-4
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ Rultor.com](https://www.rultor.com/b/dartoos-dev/json_cache)](https://www.rultor
2727
- [Suggested Dependency Relationship](#suggested-dependency-relationship)
2828
- [Implementations](#implementations)
2929
- [JsonCacheMem — Thread-safe In-memory cache](#jsoncachemem)
30+
- [JsonCacheTry — Enhanced Diagnostic Messages](#jsoncachetry)
3031
- [JsonCachePrefs — SharedPreferences](#jsoncacheprefs)
3132
- [JsonCacheEncPrefs — EncryptedSharedPreferences](#jsoncacheencprefs)
3233
- [JsonCacheLocalStorage — LocalStorage](#jsoncachelocalstorage)
@@ -194,9 +195,10 @@ device.
194195

195196
#### Typical Usage
196197

197-
Due to the fact that `JsonCacheMem` is a [Decorator](https://en.wikipedia.org/wiki/Decorator_pattern),
198-
you should normally pass another `JsonCache` instance to it whenever you instantiate a `JsonCacheMem`
199-
object. For example:
198+
Since `JsonCacheMem` is a
199+
[Decorator](https://en.wikipedia.org/wiki/Decorator_pattern), you should
200+
normally pass another `JsonCache` instance to it whenever you instantiate a
201+
`JsonCacheMem` object. For example:
200202

201203
```dart
202204
@@ -232,11 +234,34 @@ the internal in-memory cache and the level2 cache.
232234
```dart
233235
234236
final LocalStorage storage = LocalStorage('my_data');
235-
final Map<String, Map<String, dynamic>?> initData = await fetchInfo();
237+
final Map<String, Map<String, dynamic>?> initData = await fetchData();
236238
final JsonCacheMem jsonCache = JsonCacheMem.init(initData, level2:JsonCacheLocalStorage(storage));
237239
238240
```
239241

242+
### JsonCacheTry
243+
244+
[JsonCacheTry](https://pub.dev/documentation/json_cache/latest/json_cache/JsonCacheTry-class.html)
245+
is an implementation of the `JsonCache` interface whose sole purpose is to
246+
supply enhanced diagnostic information when a cache failure occurs. It does this
247+
by throwing a
248+
[JsonCacheException](https://pub.dev/documentation/json_cache/latest/json_cache/JsonCacheException-class.html)
249+
exception.
250+
251+
Since `JsonCacheTry` is a
252+
[Decorator](https://en.wikipedia.org/wiki/Decorator_pattern), you must pass
253+
another `JsonCache` instance to it whenever you instantiate a `JsonCacheTry`
254+
object. For example:
255+
256+
```dart
257+
258+
// Local storage cache initialization
259+
final prefs = await SharedPreferences.getInstance();
260+
// JsonCacheTry instance initialized with in-memory and local storage caches.
261+
final jsonCacheTry = JsonCacheTry(JsonCacheMem(JsonCachePrefs(prefs)));
262+
263+
```
264+
240265
### JsonCachePrefs
241266

242267
[JsonCachePrefs](https://pub.dev/documentation/json_cache/latest/json_cache/JsonCachePrefs-class.html)

lib/json_cache.dart

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ library json_cache;
44
export 'src/json_cache.dart';
55
export 'src/json_cache_cross_local_storage.dart';
66
export 'src/json_cache_enc_prefs.dart';
7+
export 'src/json_cache_exception.dart';
78
export 'src/json_cache_fake.dart';
89
export 'src/json_cache_hive.dart';
910
export 'src/json_cache_hollow.dart';

lib/src/json_cache.dart

+3-3
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ abstract class JsonCache {
99
/// Frees up storage space — deletes all keys and values.
1010
Future<void> clear();
1111

12-
/// Removes cached data located at [key].
12+
/// Removes the cached data located at [key].
1313
Future<void> remove(String key);
1414

15-
/// Retrieves cached data located at [key] or `null` if a cache miss occurs.
15+
/// Retrieves the data located at [key] or `null` if a cache miss occurs.
1616
Future<Map<String, dynamic>?> value(String key);
1717

1818
/// It either updates the data found at [key] with [value] or, if there is no
@@ -21,7 +21,7 @@ abstract class JsonCache {
2121
/// **Note**: [value] must be json encodable.
2222
Future<void> refresh(String key, Map<String, dynamic> value);
2323

24-
/// Checks for cached data located at [key].
24+
/// Checks for cached data at [key].
2525
///
2626
/// Returns `true` if there is cached data at [key]; `false` otherwise.
2727
Future<bool> contains(String key);

lib/src/json_cache_exception.dart

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/// An exception to indicate cache operation failures.
2+
class JsonCacheException<T extends Object> implements Exception {
3+
/// Sets [extra] as aditional information and [exception] as the original
4+
/// exception.
5+
const JsonCacheException({required this.extra, this.exception});
6+
7+
/// Additional information about cache operation failure.
8+
///
9+
/// Its `toString` method will be part of this exception's error message.
10+
final T extra;
11+
12+
/// The original exception that indicated the failure of the caching
13+
/// operation.
14+
final Exception? exception;
15+
16+
/// Returns [extra] along with the original exception message.
17+
@override
18+
String toString() => '$extra$_separator$_originalExceptionMessage';
19+
20+
/// Returns '\n' if [exception] is set; otherwise, the empty string ''.
21+
String get _separator => exception != null ? '\n' : '';
22+
23+
/// Returns the exception message if [exception] is set; otherwise, the empty
24+
/// string ''.
25+
String get _originalExceptionMessage => exception?.toString() ?? '';
26+
}

lib/src/json_cache_mem.dart

+41-40
Original file line numberDiff line numberDiff line change
@@ -10,51 +10,51 @@ typedef OnInitError = FutureOr<Null> Function(Object, StackTrace);
1010

1111
/// Thread-safe in-memory [JsonCache] decorator.
1212
///
13-
/// It is a kind of level 1 cache.
13+
/// It is a kind of _level 1_ cache.
1414
///
1515
/// It encapsulates a slower cache and keeps its own data in-memory.
1616
class JsonCacheMem implements JsonCache {
17-
/// In-memory level1 cache with an optional level2 instance.
17+
/// In-memory _level 1_ cache with an optional _level 2_ instance.
1818
///
1919
/// **Note**: if you do not pass an object to the parameter [level2], the data
2020
/// will remain cached in-memory only; that is, no data will be persited to
21-
/// the user's device's local storage area. Indeed, not persisting data on the
21+
/// the local storage of the user's device. Indeed, not persisting data on the
2222
/// user's device might be the desired behavior if you are at the prototyping
2323
/// or mocking phase. However, its unlikely to be the right behavior in
2424
/// production code.
2525
JsonCacheMem([JsonCache? level2])
2626
: this.mem(_shrMem, level2: level2, mutex: _shrMutex);
2727

28-
/// Initializes both the level1 (internal memory) and level2 (user's device's
29-
/// local storage area) caches with data.
28+
/// Initializes both the internal memory (level 1 cache) and the local storage
29+
/// of the user's device (level 2 cache — [level2]) with the contents of
30+
/// [initData].
3031
///
31-
/// It also provides a type of transaction guarantee whereby, if an error
32-
/// occurs while copying [init] to cache, it will try to revert the cached
33-
/// data to its previous state before rethrowing the exception that signaled
34-
/// the error. Finally, after reverting the cached data, it invokes
35-
/// [onInitError].
32+
/// This method also provides a kind of transaction guarantee whereby if an
33+
/// error occurs while copying [initData] to the cache, it will attempt to
34+
/// revert the cache [level2] to its previous state before rethrowing the
35+
/// exception that signaled the error. Finally, after reverting the cached
36+
/// data, it will invoke [onInitError].
3637
JsonCacheMem.init(
37-
Map<String, Map<String, dynamic>?> init, {
38+
Map<String, Map<String, dynamic>?> initData, {
3839
JsonCache? level2,
3940
OnInitError? onInitError,
4041
}) : _level2 = level2 ?? const JsonCacheHollow(),
4142
_memory = _shrMem,
4243
_mutex = _shrMutex {
43-
final copy = Map<String, Map<String, dynamic>?>.of(init);
4444
final initFut = _mutex.protectWrite(() async {
45-
final writes = <String>[]; // the list of written keys.
46-
for (final entry in copy.entries) {
45+
final cachedKeys = <String>[];
46+
for (final entry in initData.entries) {
4747
final key = entry.key;
4848
final value = entry.value;
4949
if (value != null) {
50-
writes.add(key);
50+
cachedKeys.add(key);
5151
try {
5252
await _level2.refresh(key, value);
5353
_memory[key] = value;
5454
} catch (error) {
55-
for (final write in writes) {
56-
await _level2.remove(write);
57-
_memory.remove(write);
55+
for (final key in cachedKeys) {
56+
await _level2.remove(key);
57+
_memory.remove(key);
5858
}
5959
rethrow;
6060
}
@@ -77,7 +77,7 @@ class JsonCacheMem implements JsonCache {
7777
_level2 = level2 ?? const JsonCacheHollow(),
7878
_mutex = mutex ?? ReadWriteMutex();
7979

80-
/// Slower cache level.
80+
/// Slower level 2 cache.
8181
final JsonCache _level2;
8282

8383
/// In-memory storage.
@@ -93,6 +93,8 @@ class JsonCacheMem implements JsonCache {
9393
static final _shrMutex = ReadWriteMutex();
9494

9595
/// Frees up storage space in both the level2 cache and in-memory cache.
96+
///
97+
/// Throws [JsonCacheException] to indicate operation failure.
9698
@override
9799
Future<void> clear() async {
98100
await _mutex.protectWrite(() async {
@@ -101,15 +103,17 @@ class JsonCacheMem implements JsonCache {
101103
});
102104
}
103105

104-
/// Updates data located at [key] in both the level2 cache and in-memory
105-
/// cache.
106+
/// Updates the data located at [key] in both the _level 2_ cache and
107+
/// in-memory cache.
108+
///
109+
/// Throws [JsonCacheException] to indicate operation failure.
106110
@override
107111
Future<void> refresh(String key, Map<String, dynamic> data) async {
108112
// ATTENTION: It is safer to copy the content of [data] before calling an
109113
// asynchronous method that will copy it to avoid data races. For example,
110114
// if the client code clears [data] right after passing it to this method,
111-
// there's a high chance of having _level2 and this object with different
112-
// contents.
115+
// there's a high chance of having the level 2 cache and this object with
116+
// different contents.
113117
//
114118
// In Dart, synchronous code cannot be interrupted, so there is no need to
115119
// protect it using mutual exclusion.
@@ -120,8 +124,8 @@ class JsonCacheMem implements JsonCache {
120124
});
121125
}
122126

123-
/// Removes the value located at [key] from both the level2 cache and
124-
/// in-memory cache.
127+
/// Removes the cached value located at [key] from both the _level 2 cache_
128+
/// and _in-memory cache_.
125129
@override
126130
Future<void> remove(String key) async {
127131
await _mutex.protectWrite(() async {
@@ -130,33 +134,30 @@ class JsonCacheMem implements JsonCache {
130134
});
131135
}
132136

133-
/// Retrieves the value at [key] or null if there is no data.
137+
/// Retrieves the value at [key] or `null` if there is no data.
134138
@override
135139
Future<Map<String, dynamic>?> value(String key) {
136140
return _mutex.protectRead(() async {
137-
final cachedL1 = _memory[key];
138-
if (cachedL1 != null) {
139-
return Map<String, dynamic>.of(cachedL1);
141+
final inMemoryValue = _memory[key];
142+
if (inMemoryValue != null) {
143+
return Map<String, dynamic>.of(inMemoryValue);
140144
}
141-
final cachedL2 = await _level2.value(key);
142-
if (cachedL2 != null) {
143-
_memory[key] = cachedL2;
144-
return Map<String, dynamic>.of(cachedL2);
145+
final localStorageValue = await _level2.value(key);
146+
if (localStorageValue != null) {
147+
_memory[key] = localStorageValue;
148+
return Map<String, dynamic>.of(localStorageValue);
145149
}
146150
return null;
147151
});
148152
}
149153

150-
/// Checks whether the in-memory or the level2 chache contains cached data at
151-
/// [key].
154+
/// Checks whether the _in-memory_ or the _level 2 cache_ contains cached data
155+
/// at [key].
152156
@override
153157
Future<bool> contains(String key) {
154158
return _mutex.protectRead(() async {
155-
bool found = _memory.containsKey(key);
156-
if (!found) {
157-
found = await _level2.contains(key);
158-
}
159-
return found;
159+
final bool foundInMemory = _memory.containsKey(key);
160+
return foundInMemory ? foundInMemory : await _level2.contains(key);
160161
});
161162
}
162163
}

lib/src/json_cache_try.dart

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import 'package:json_cache/json_cache.dart';
2+
3+
/// A [JsonCache] decorator that provides improved information about
4+
/// cache-related failures by throwing [JsonCacheException].
5+
class JsonCacheTry implements JsonCache {
6+
/// Sets [wrapped] as the instance to which this object will forward all
7+
/// method calls.
8+
///
9+
/// **Precondition**: the type of [wrapped] must not be `JsonCacheTry`.
10+
const JsonCacheTry(JsonCache wrapped)
11+
: assert(wrapped is! JsonCacheTry),
12+
_wrapped = wrapped;
13+
14+
/// The JsonCache instance to which this object will forward all method calls.
15+
final JsonCache _wrapped;
16+
17+
/// Frees up storage space by clearing cached data.
18+
///
19+
/// Throws [JsonCacheException] to indicate operation failure.
20+
@override
21+
Future<void> clear() async {
22+
try {
23+
await _wrapped.clear();
24+
} on Exception catch (ex) {
25+
throw JsonCacheException(
26+
extra: 'Error clearing cached data.',
27+
exception: ex,
28+
);
29+
}
30+
}
31+
32+
/// Updates the data located at [key].
33+
///
34+
/// Throws [JsonCacheException] to indicate operation failure.
35+
@override
36+
Future<void> refresh(String key, Map<String, dynamic> value) async {
37+
try {
38+
await _wrapped.refresh(key, value);
39+
} on Exception catch (ex) {
40+
throw JsonCacheException(
41+
extra: "Error refreshing cached data at index '$key'.",
42+
exception: ex,
43+
);
44+
}
45+
}
46+
47+
/// Removes the cached data located at [key].
48+
///
49+
/// Throws [JsonCacheException] to indicate operation failure.
50+
@override
51+
Future<void> remove(String key) async {
52+
try {
53+
await _wrapped.remove(key);
54+
} on Exception catch (ex) {
55+
throw JsonCacheException(
56+
extra: "Error removing cached data at index '$key'.",
57+
exception: ex,
58+
);
59+
}
60+
}
61+
62+
/// Retrieves the value located at [key] or `null` if there is no data.
63+
///
64+
/// Throws [JsonCacheException] to indicate operation failure.
65+
@override
66+
Future<Map<String, dynamic>?> value(String key) async {
67+
try {
68+
return await _wrapped.value(key);
69+
} on Exception catch (ex) {
70+
throw JsonCacheException(
71+
extra: "Error retrieving cached data at index '$key'.",
72+
exception: ex,
73+
);
74+
}
75+
}
76+
77+
/// Checks for cached data at [key].
78+
///
79+
/// Throws [JsonCacheException] to indicate operation failure.
80+
@override
81+
Future<bool> contains(String key) async {
82+
try {
83+
return await _wrapped.contains(key);
84+
} on Exception catch (ex) {
85+
throw JsonCacheException(
86+
extra: "Error checking for cached data at index '$key'.",
87+
exception: ex,
88+
);
89+
}
90+
}
91+
}

0 commit comments

Comments
 (0)