-
Notifications
You must be signed in to change notification settings - Fork 31
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1602 from nextcloud/feat/neon_framework/add_persi…
…stence_backaend_for_storages refactor(neon_framework): use a separate persistence layer backing th…
- Loading branch information
Showing
10 changed files
with
456 additions
and
113 deletions.
There are no files selected for viewing
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
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,7 @@ deeplinking | |
flathub | ||
foss | ||
fullscreen | ||
persistences | ||
playstore | ||
postmarket | ||
provokateurin | ||
|
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
import 'dart:async'; | ||
|
||
import 'package:meta/meta.dart' show protected; | ||
|
||
/// A persistent key value storage. | ||
abstract interface class Persistence<T extends Object> { | ||
/// Whether a value exists at the given [key]. | ||
FutureOr<bool> containsKey(String key); | ||
|
||
/// Clears all values from persistent storage. | ||
FutureOr<bool> clear(); | ||
|
||
/// Removes an entry from persistent storage. | ||
FutureOr<bool> remove(String key); | ||
|
||
/// Saves a [value] to persistent storage. | ||
FutureOr<bool> setValue(String key, T value); | ||
|
||
/// Fetches the value persisted at the given [key] from the persistent | ||
/// storage. | ||
FutureOr<T?> getValue(String key); | ||
} | ||
|
||
/// A key value persistence that caches read values to be accessed | ||
/// synchronously. | ||
/// | ||
/// Mutating values is asynchronous. | ||
abstract class CachedPersistence<T extends Object> implements Persistence<T> { | ||
/// Fetches the latest values from the host platform. | ||
/// | ||
/// Use this method to observe modifications that were made in the background | ||
/// like another isolate or native code while the app is already running. | ||
Future<void> reload(); | ||
|
||
/// The cache that holds all values. | ||
/// | ||
/// It is instantiated to the current state of the backing database and then | ||
/// kept in sync via setter methods in this class. | ||
/// | ||
/// It is NOT guaranteed that this cache and the backing database will remain | ||
/// in sync since the setter method might fail for any reason. | ||
@protected | ||
final Map<String, T> cache = {}; | ||
|
||
@override | ||
T? getValue(String key) => cache[key]; | ||
|
||
/// Saves a [value] to the cached storage. | ||
/// | ||
/// Use this method to cache type conversions of the value that do not change | ||
/// the meaning of the actual value like a `BuiltList` to `List` conversion. | ||
/// Changes will not be persisted to the backing storage and will be lost | ||
/// when the app is restarted. | ||
void setCache(String key, T value) => cache[key] = value; | ||
|
||
@override | ||
bool containsKey(String key) => cache.containsKey(key); | ||
|
||
@override | ||
Future<bool> clear(); | ||
|
||
@override | ||
Future<bool> remove(String key); | ||
|
||
@override | ||
Future<bool> setValue(String key, T value); | ||
} |
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
139 changes: 139 additions & 0 deletions
139
packages/neon_framework/lib/src/storage/shared_preferences_persistence.dart
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
// ignore_for_file: cascade_invocations | ||
|
||
import 'package:meta/meta.dart'; | ||
import 'package:neon_framework/src/storage/persistence.dart'; | ||
import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; | ||
import 'package:shared_preferences_platform_interface/types.dart'; | ||
|
||
/// The version of the [SharedPreferencesPersistence]. | ||
/// | ||
/// Needed to make potential migrations in the future. | ||
@visibleForTesting | ||
const int kSharedPreferenceVersion = 1; | ||
|
||
/// The default prefix used by `SharedPreferences`. | ||
/// | ||
/// Used for legacy reasons to allow seamless upgrades. | ||
// TODO: replace with our packageID | ||
const String _defaultPrefix = 'flutter.'; | ||
|
||
/// An SharedPreferences backed cached persistence for preferences. | ||
/// | ||
/// There is only one cache backing all `SharedPreferencesPersistence` | ||
/// instances. Use the [_prefix] to separate different storages. | ||
/// Keys within a storage must be unique. | ||
@internal | ||
final class SharedPreferencesPersistence implements CachedPersistence { | ||
/// Creates a new SharedPreferences persistence. | ||
const SharedPreferencesPersistence({String prefix = ''}) | ||
: _prefix = prefix == '' ? _defaultPrefix : '$_defaultPrefix$prefix-'; | ||
|
||
static SharedPreferencesStorePlatform get _store => SharedPreferencesStorePlatform.instance; | ||
|
||
/// The prefix of this persistence. | ||
/// | ||
/// Keys within it must be unique. | ||
@protected | ||
final String _prefix; | ||
|
||
@override | ||
Map<String, Object> get cache => _globalCache; | ||
|
||
static final Map<String, Object> _globalCache = {}; | ||
|
||
static bool _initialized = false; | ||
|
||
/// Initializes all persistences by setting up the backing SharedPreferences | ||
/// storage and priming the global cache. | ||
/// | ||
/// This must be called and completed before any calls to persistence are made. | ||
static Future<void> init() async { | ||
if (_initialized) { | ||
return; | ||
} | ||
|
||
final fromSystem = await _store.getAll(); | ||
_globalCache.addAll(fromSystem); | ||
|
||
const versionKey = 'neon-version'; | ||
const persistence = SharedPreferencesPersistence(); | ||
if (!persistence.containsKey(versionKey)) { | ||
await persistence.setValue(versionKey, kSharedPreferenceVersion); | ||
} | ||
|
||
_initialized = true; | ||
} | ||
|
||
/// Resets class's static values to allow for testing multiple init calls. | ||
@visibleForTesting | ||
static void resetStatic() { | ||
_globalCache.clear(); | ||
_initialized = false; | ||
} | ||
|
||
@override | ||
Object? getValue(String key) { | ||
final prefixedKey = '$_prefix$key'; | ||
return cache[prefixedKey]; | ||
} | ||
|
||
@override | ||
void setCache(String key, Object value) { | ||
final prefixedKey = '$_prefix$key'; | ||
cache[prefixedKey] = value; | ||
} | ||
|
||
@override | ||
bool containsKey(String key) { | ||
final prefixedKey = '$_prefix$key'; | ||
|
||
return cache.containsKey(prefixedKey); | ||
} | ||
|
||
@override | ||
Future<bool> clear() { | ||
cache.removeWhere((key, _) => key.startsWith(_prefix)); | ||
|
||
return _store.clearWithParameters( | ||
ClearParameters( | ||
filter: PreferencesFilter(prefix: _prefix), | ||
), | ||
); | ||
} | ||
|
||
@override | ||
Future<void> reload() async { | ||
final fromSystem = await _store.getAllWithParameters( | ||
GetAllParameters( | ||
filter: PreferencesFilter(prefix: _prefix), | ||
), | ||
); | ||
|
||
cache.removeWhere((key, _) => key.startsWith(_prefix)); | ||
cache.addAll(fromSystem); | ||
} | ||
|
||
@override | ||
Future<bool> remove(String key) { | ||
final prefixedKey = '$_prefix$key'; | ||
|
||
cache.remove(prefixedKey); | ||
return _store.remove(prefixedKey); | ||
} | ||
|
||
@override | ||
Future<bool> setValue(String key, Object value) { | ||
final prefixedKey = '$_prefix$key'; | ||
|
||
cache[prefixedKey] = value; | ||
return switch (value) { | ||
int _ => _store.setValue('Int', prefixedKey, value), | ||
double _ => _store.setValue('Double', prefixedKey, value), | ||
String _ => _store.setValue('String', prefixedKey, value), | ||
bool _ => _store.setValue('Bool', prefixedKey, value), | ||
// Make a copy of the list so that later mutations won't propagate | ||
Iterable<String> _ => _store.setValue('StringList', prefixedKey, value.toList()), | ||
Object() => throw ArgumentError.value(value, 'value', 'Unsupported type.'), | ||
}; | ||
} | ||
} |
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
Oops, something went wrong.