Skip to content

Commit

Permalink
Merge pull request #1602 from nextcloud/feat/neon_framework/add_persi…
Browse files Browse the repository at this point in the history
…stence_backaend_for_storages

refactor(neon_framework): use a separate persistence layer backing th…
  • Loading branch information
Leptopoda authored Feb 25, 2024
2 parents b8313f1 + 786c29c commit b1e52e1
Show file tree
Hide file tree
Showing 10 changed files with 456 additions and 113 deletions.
1 change: 1 addition & 0 deletions .cspell/misc.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ deeplinking
flathub
foss
fullscreen
persistences
playstore
postmarket
provokateurin
Expand Down
67 changes: 67 additions & 0 deletions packages/neon_framework/lib/src/storage/persistence.dart
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);
}
58 changes: 11 additions & 47 deletions packages/neon_framework/lib/src/storage/settings_store.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
// ignore_for_file: avoid_positional_boolean_parameters

import 'package:meta/meta.dart';
import 'package:neon_framework/src/storage/keys.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:neon_framework/src/storage/persistence.dart';

/// A storage that can save a group of values primarily used by `Option`s.
///
Expand All @@ -12,14 +11,6 @@ import 'package:shared_preferences/shared_preferences.dart';
/// See:
/// * `NeonStorage` to initialize and manage different storage backends.
abstract interface class SettingsStore {
/// The group key for this app storage.
StorageKeys get groupKey;

/// The optional suffix of the storage key.
///
/// Used to differentiate between multiple AppStorages with the same [groupKey].
String? get suffix;

/// The id that uniquely identifies this app storage.
///
/// Used in `Exportable` classes.
Expand All @@ -43,59 +34,32 @@ abstract interface class SettingsStore {
Future<bool> remove(String key);
}

/// Default implementation of the [SettingsStore] backed by the given [database].
/// Default implementation of the [SettingsStore] backed by the given [persistence].
@immutable
@internal
final class DefaultSettingsStore implements SettingsStore {
/// Creates a new app storage.
const DefaultSettingsStore(
this.database,
this.groupKey, [
this.suffix,
]);
const DefaultSettingsStore(this.persistence, this.id);

/// The cached persistence backing this storage.
@protected
final SharedPreferences database;

/// The group key for this app storage.
///
/// Keys are formatted with [formatKey]
@override
final StorageKeys groupKey;
final CachedPersistence persistence;

@override
final String? suffix;

/// Returns the id for this app storage.
///
/// Uses the [suffix] and falling back to the [groupKey] if not present.
/// This uniquely identifies the storage and is used in `Exportable` classes.
@override
String get id => suffix ?? groupKey.value;

/// Concatenates the [groupKey], [suffix] and [key] to build a unique key
/// used in the storage backend.
@visibleForTesting
String formatKey(String key) {
if (suffix != null) {
return '${groupKey.value}-$suffix-$key';
}

return '${groupKey.value}-$key';
}
final String id;

@override
Future<bool> remove(String key) => database.remove(formatKey(key));
Future<bool> remove(String key) => persistence.remove(key);

@override
String? getString(String key) => database.getString(formatKey(key));
String? getString(String key) => persistence.getValue(key) as String?;

@override
Future<bool> setString(String key, String value) => database.setString(formatKey(key), value);
Future<bool> setString(String key, String value) => persistence.setValue(key, value);

@override
bool? getBool(String key) => database.getBool(formatKey(key));
bool? getBool(String key) => persistence.getValue(key) as bool?;

@override
Future<bool> setBool(String key, bool value) => database.setBool(formatKey(key), value);
Future<bool> setBool(String key, bool value) => persistence.setValue(key, value);
}
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.'),
};
}
}
37 changes: 24 additions & 13 deletions packages/neon_framework/lib/src/storage/single_value_store.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import 'package:built_collection/built_collection.dart';
import 'package:meta/meta.dart';
import 'package:neon_framework/src/storage/keys.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:neon_framework/src/storage/persistence.dart';

/// A storage that itself is a single entry of a key value store.
///
Expand Down Expand Up @@ -44,40 +44,51 @@ abstract interface class SingleValueStore {
Future<bool> setStringList(BuiltList<String> value);
}

/// Default implementation of the [SingleValueStore] backed by the given [database].
/// Default implementation of the [SingleValueStore] backed by the given [persistence].
@immutable
@internal
final class DefaultSingleValueStore implements SingleValueStore {
/// Creates a new storage for a single value.
const DefaultSingleValueStore(this.database, this.key);
const DefaultSingleValueStore(this.persistence, this.key);

/// The cached persistence backing this storage.
@protected
final SharedPreferences database;
final CachedPersistence persistence;

@override
final StorageKeys key;

@override
bool hasValue() => database.containsKey(key.value);
bool hasValue() => persistence.containsKey(key.value);

@override
Future<bool> remove() => database.remove(key.value);
Future<bool> remove() => persistence.remove(key.value);

@override
String? getString() => database.getString(key.value);

String? getString() => persistence.getValue(key.value) as String?;
@override
Future<bool> setString(String value) => database.setString(key.value, value);
Future<bool> setString(String value) => persistence.setValue(key.value, value);

@override
bool? getBool() => database.getBool(key.value);
bool? getBool() => persistence.getValue(key.value) as bool?;

@override
Future<bool> setBool(bool value) => database.setBool(key.value, value);
Future<bool> setBool(bool value) => persistence.setValue(key.value, value);

@override
BuiltList<String>? getStringList() => database.getStringList(key.value)?.toBuiltList();
BuiltList<String>? getStringList() {
final key = this.key.value;
var list = persistence.getValue(key) as Iterable?;

if (list is! BuiltList<String>?) {
list = BuiltList<String>.from(list);
persistence.setCache(key, list);
return list;
}

return list;
}

@override
Future<bool> setStringList(BuiltList<String> value) => database.setStringList(key.value, value.toList());
Future<bool> setStringList(BuiltList<String> value) => persistence.setValue(key.value, value);
}
Loading

0 comments on commit b1e52e1

Please sign in to comment.