store.dart 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. import 'package:collection/collection.dart';
  2. import 'package:immich_mobile/shared/models/user.dart';
  3. import 'package:isar/isar.dart';
  4. part 'store.g.dart';
  5. /// Key-value store for individual items enumerated in StoreKey.
  6. /// Supports String, int and JSON-serializable Objects
  7. /// Can be used concurrently from multiple isolates
  8. class Store {
  9. static late final Isar _db;
  10. static final List<dynamic> _cache =
  11. List.filled(StoreKey.values.map((e) => e.id).max + 1, null);
  12. /// Initializes the store (call exactly once per app start)
  13. static void init(Isar db) {
  14. _db = db;
  15. _populateCache();
  16. _db.storeValues.where().build().watch().listen(_onChangeListener);
  17. }
  18. /// clears all values from this store (cache and DB), only for testing!
  19. static Future<void> clear() {
  20. _cache.fillRange(0, _cache.length, null);
  21. return _db.writeTxn(() => _db.storeValues.clear());
  22. }
  23. /// Returns the stored value for the given key or if null the [defaultValue]
  24. /// Throws a [StoreKeyNotFoundException] if both are null
  25. static T get<T>(StoreKey<T> key, [T? defaultValue]) {
  26. final value = _cache[key.id] ?? defaultValue;
  27. if (value == null) {
  28. throw StoreKeyNotFoundException(key);
  29. }
  30. return value;
  31. }
  32. /// Returns the stored value for the given key (possibly null)
  33. static T? tryGet<T>(StoreKey<T> key) => _cache[key.id];
  34. /// Stores the value synchronously in the cache and asynchronously in the DB
  35. static Future<void> put<T>(StoreKey<T> key, T value) {
  36. _cache[key.id] = value;
  37. return _db.writeTxn(
  38. () async => _db.storeValues.put(await StoreValue._of(value, key)),
  39. );
  40. }
  41. /// Removes the value synchronously from the cache and asynchronously from the DB
  42. static Future<void> delete<T>(StoreKey<T> key) {
  43. _cache[key.id] = null;
  44. return _db.writeTxn(() => _db.storeValues.delete(key.id));
  45. }
  46. /// Fills the cache with the values from the DB
  47. static _populateCache() {
  48. for (StoreKey key in StoreKey.values) {
  49. final StoreValue? value = _db.storeValues.getSync(key.id);
  50. if (value != null) {
  51. _cache[key.id] = value._extract(key);
  52. }
  53. }
  54. }
  55. /// updates the state if a value is updated in any isolate
  56. static void _onChangeListener(List<StoreValue>? data) {
  57. if (data != null) {
  58. for (StoreValue value in data) {
  59. _cache[value.id] =
  60. value._extract(StoreKey.values.firstWhere((e) => e.id == value.id));
  61. }
  62. }
  63. }
  64. }
  65. /// Internal class for `Store`, do not use elsewhere.
  66. @Collection(inheritance: false)
  67. class StoreValue {
  68. StoreValue(this.id, {this.intValue, this.strValue});
  69. Id id;
  70. int? intValue;
  71. String? strValue;
  72. T? _extract<T>(StoreKey<T> key) {
  73. switch (key.type) {
  74. case int:
  75. return intValue as T?;
  76. case bool:
  77. return intValue == null ? null : (intValue! == 1) as T;
  78. case DateTime:
  79. return intValue == null
  80. ? null
  81. : DateTime.fromMicrosecondsSinceEpoch(intValue!) as T;
  82. case String:
  83. return strValue as T?;
  84. default:
  85. if (key.fromDb != null) {
  86. return key.fromDb!.call(Store._db, intValue!);
  87. }
  88. }
  89. throw TypeError();
  90. }
  91. static Future<StoreValue> _of<T>(T? value, StoreKey<T> key) async {
  92. int? i;
  93. String? s;
  94. switch (key.type) {
  95. case int:
  96. i = value as int?;
  97. break;
  98. case bool:
  99. i = value == null ? null : (value == true ? 1 : 0);
  100. break;
  101. case DateTime:
  102. i = value == null ? null : (value as DateTime).microsecondsSinceEpoch;
  103. break;
  104. case String:
  105. s = value as String?;
  106. break;
  107. default:
  108. if (key.toDb != null) {
  109. i = await key.toDb!.call(Store._db, value);
  110. break;
  111. }
  112. throw TypeError();
  113. }
  114. return StoreValue(key.id, intValue: i, strValue: s);
  115. }
  116. }
  117. class StoreKeyNotFoundException implements Exception {
  118. final StoreKey key;
  119. StoreKeyNotFoundException(this.key);
  120. @override
  121. String toString() => "Key '${key.name}' not found in Store";
  122. }
  123. /// Key for each possible value in the `Store`.
  124. /// Defines the data type for each value
  125. enum StoreKey<T> {
  126. version<int>(0, type: int),
  127. assetETag<String>(1, type: String),
  128. currentUser<User>(2, type: User, fromDb: _getUser, toDb: _toUser),
  129. deviceIdHash<int>(3, type: int),
  130. deviceId<String>(4, type: String),
  131. backupFailedSince<DateTime>(5, type: DateTime),
  132. backupRequireWifi<bool>(6, type: bool),
  133. backupRequireCharging<bool>(7, type: bool),
  134. backupTriggerDelay<int>(8, type: int),
  135. githubReleaseInfo<String>(9, type: String),
  136. serverUrl<String>(10, type: String),
  137. accessToken<String>(11, type: String),
  138. serverEndpoint<String>(12, type: String),
  139. autoBackup<bool>(13, type: bool),
  140. // user settings from [AppSettingsEnum] below:
  141. loadPreview<bool>(100, type: bool),
  142. loadOriginal<bool>(101, type: bool),
  143. themeMode<String>(102, type: String),
  144. tilesPerRow<int>(103, type: int),
  145. dynamicLayout<bool>(104, type: bool),
  146. groupAssetsBy<int>(105, type: int),
  147. uploadErrorNotificationGracePeriod<int>(106, type: int),
  148. backgroundBackupTotalProgress<bool>(107, type: bool),
  149. backgroundBackupSingleProgress<bool>(108, type: bool),
  150. storageIndicator<bool>(109, type: bool),
  151. thumbnailCacheSize<int>(110, type: int),
  152. imageCacheSize<int>(111, type: int),
  153. albumThumbnailCacheSize<int>(112, type: int),
  154. selectedAlbumSortOrder<int>(113, type: int),
  155. advancedTroubleshooting<bool>(114, type: bool),
  156. logLevel<int>(115, type: int),
  157. ;
  158. const StoreKey(
  159. this.id, {
  160. required this.type,
  161. this.fromDb,
  162. this.toDb,
  163. });
  164. final int id;
  165. final Type type;
  166. final T? Function<T>(Isar, int)? fromDb;
  167. final Future<int> Function<T>(Isar, T)? toDb;
  168. }
  169. T? _getUser<T>(Isar db, int i) {
  170. final User? u = db.users.getSync(i);
  171. return u as T?;
  172. }
  173. Future<int> _toUser<T>(Isar db, T u) {
  174. if (u is User) {
  175. return db.users.put(u);
  176. }
  177. throw TypeError();
  178. }