LCOV - code coverage report
Current view: top level - src - the_storage.dart Coverage Total Hit
Test: filtered_lcov.info Lines: 98.7 % 159 157
Test Date: 2026-04-08 10:01:09 Functions: - 0 0

            Line data    Source code
       1              : import 'dart:async';
       2              : 
       3              : import 'package:flutter/foundation.dart';
       4              : import 'package:logging/logging.dart';
       5              : import 'package:rxdart/rxdart.dart';
       6              : import 'package:the_storage/src/abstract_storage.dart';
       7              : import 'package:the_storage/src/cipher_storage.dart';
       8              : import 'package:the_storage/src/db/storage_database.dart';
       9              : import 'package:the_storage/src/encrypt_helper.dart';
      10              : import 'package:the_storage/src/reactive_storage.dart';
      11              : import 'package:the_storage/src/storage.dart';
      12              : 
      13              : /// The storage interface
      14              : typedef TheStorageInterface = AbstractStorage<String>;
      15              : 
      16              : /// The reactive interface
      17              : typedef ReactiveInterface = ReactiveStorage<String>;
      18              : 
      19              : /// The storage: fast and secure
      20              : class TheStorage implements TheStorageInterface, ReactiveInterface {
      21              :   /// Get storage instance
      22            2 :   factory TheStorage.i() {
      23            2 :     _instance ??= TheStorage._();
      24              :     return _instance!;
      25              :   }
      26            2 :   TheStorage._();
      27              :   static TheStorage? _instance;
      28              : 
      29              :   final _log = Logger('TheLogger');
      30              : 
      31              :   final _cipherStorage = CipherStorage();
      32              :   late EncryptHelper _encryptHelper;
      33              :   final _storage = Storage();
      34              : 
      35              :   bool _initialized = false;
      36              : 
      37              :   /// (key, domain, defaultValue, keepAlive) -> BehaviorSubject of value
      38              :   final Map<(String, String, String?, bool), BehaviorSubject<String?>>
      39              :   _subjects = {};
      40              : 
      41              :   /// (domain, keepAlive) -> BehaviorSubject of domain
      42              :   final Map<(String, bool), BehaviorSubject<Map<String, String>>>
      43              :   _domainSubjects = {};
      44              : 
      45              :   /// (domain, keepAlive) -> BehaviorSubject of domain keys
      46              :   final Map<(String, bool), BehaviorSubject<List<String>>> _domainKeysSubjects =
      47              :       {};
      48              : 
      49              :   /// Init encrypted storage
      50            2 :   Future<void> init([
      51              :     String dbName = AbstractStorage.storageFileName,
      52              :     @visibleForTesting StorageDatabase? database,
      53              :   ]) async {
      54            2 :     if (_initialized) {
      55            0 :       _log.warning('TheStorage is already initialized!');
      56              :       return;
      57              :     }
      58              : 
      59            2 :     _initialized = true;
      60              : 
      61            4 :     await Future.wait([
      62            4 :       _cipherStorage.init(),
      63            4 :       _storage.init(dbName, database),
      64              :     ]);
      65            6 :     _encryptHelper = EncryptHelper(_cipherStorage);
      66              :   }
      67              : 
      68              :   /// Dispose storage
      69            2 :   Future<void> dispose() async {
      70            2 :     _assureInitialized();
      71              : 
      72            2 :     _clearSubjects();
      73              : 
      74            4 :     await _storage.dispose();
      75              : 
      76              :     _instance = null;
      77              :   }
      78              : 
      79            2 :   @override
      80              :   Future<void> reset() async {
      81            2 :     _assureInitialized();
      82              : 
      83            4 :     await _storage.reset();
      84              : 
      85            2 :     _notifyClearAll();
      86              : 
      87            2 :     await dispose();
      88              :     // _initialized = false;
      89              :   }
      90              : 
      91            2 :   @override
      92              :   Future<void> clearAll() async {
      93            2 :     _assureInitialized();
      94              : 
      95            4 :     await _storage.clearAll();
      96              : 
      97            2 :     _notifyClearAll();
      98              :   }
      99              : 
     100            2 :   @override
     101              :   Future<void> clearDomain([
     102              :     String domain = AbstractStorage.defaultDomain,
     103              :   ]) async {
     104            2 :     _assureInitialized();
     105              : 
     106            4 :     await _storage.clearDomain(domain);
     107              : 
     108            2 :     await _notifyAllSubjects(null, domain: domain);
     109              :   }
     110              : 
     111            2 :   @override
     112              :   Future<void> delete(
     113              :     String key, {
     114              :     String domain = AbstractStorage.defaultDomain,
     115              :   }) async {
     116            2 :     _assureInitialized();
     117              : 
     118            8 :     await _storage.delete(_encryptHelper.encrypt(key), domain: domain);
     119              : 
     120            2 :     await _notifyAllSubjects(key, domain: domain);
     121              :   }
     122              : 
     123            2 :   @override
     124              :   Future<void> deleteDomain(
     125              :     List<String> keys, {
     126              :     String domain = AbstractStorage.defaultDomain,
     127              :   }) async {
     128            2 :     _assureInitialized();
     129              : 
     130            4 :     await _storage.deleteDomain(
     131           10 :       keys.map((key) => _encryptHelper.encrypt(key)).toList(),
     132              :       domain: domain,
     133              :     );
     134              : 
     135            2 :     await _notifyAllSubjects(null, domain: domain);
     136              :   }
     137              : 
     138            2 :   @override
     139              :   Future<String?> get(
     140              :     String key, {
     141              :     String? defaultValue,
     142              :     String domain = AbstractStorage.defaultDomain,
     143              :   }) async {
     144            2 :     _assureInitialized();
     145              : 
     146            4 :     final storageValue = await _storage.get(
     147            4 :       _encryptHelper.encrypt(key),
     148              :       defaultValue: defaultValue != null
     149            3 :           ? StorageValue(_encryptHelper.encrypt(defaultValue), '')
     150              :           : null,
     151              :       domain: domain,
     152              :     );
     153              : 
     154            4 :     return _encryptHelper.decryptNullable(
     155            2 :       storageValue?.value,
     156            2 :       storageValue?.iv,
     157              :     );
     158              :   }
     159              : 
     160            2 :   @override
     161              :   Future<Map<String, String>> getDomain({
     162              :     String domain = AbstractStorage.defaultDomain,
     163              :   }) async {
     164            2 :     _assureInitialized();
     165              : 
     166            4 :     final pairs = await _storage.getDomain(
     167              :       domain: domain,
     168              :     );
     169              : 
     170            2 :     return pairs.map(
     171            4 :       (key, value) => MapEntry(
     172            4 :         _encryptHelper.decrypt(key),
     173            8 :         _encryptHelper.decrypt(value.value, value.iv),
     174              :       ),
     175              :     );
     176              :   }
     177              : 
     178            2 :   @override
     179              :   Future<List<String>> getDomainKeys({
     180              :     String domain = AbstractStorage.defaultDomain,
     181              :   }) async {
     182            2 :     _assureInitialized();
     183              : 
     184            4 :     final keys = await _storage.getDomainKeys(
     185              :       domain: domain,
     186              :     );
     187              : 
     188           10 :     return keys.map((key) => _encryptHelper.decrypt(key)).toList();
     189              :   }
     190              : 
     191            2 :   @override
     192              :   Future<void> set(
     193              :     String key,
     194              :     String value, {
     195              :     String domain = AbstractStorage.defaultDomain,
     196              :     bool overwrite = true,
     197              :   }) async {
     198            2 :     _assureInitialized();
     199              : 
     200            4 :     final iv = CipherStorage.ivFromSecureRandom().base64;
     201              : 
     202            4 :     await _storage.set(
     203            4 :       _encryptHelper.encrypt(key),
     204            6 :       StorageValue(_encryptHelper.encrypt(value, iv), iv),
     205              :       domain: domain,
     206              :       overwrite: overwrite,
     207              :     );
     208              : 
     209            2 :     await _notifyAllSubjects(key, domain: domain);
     210              :   }
     211              : 
     212            2 :   @override
     213              :   Future<void> setDomain(
     214              :     Map<String, String> pairs, {
     215              :     String domain = AbstractStorage.defaultDomain,
     216              :     bool overwrite = true,
     217              :   }) async {
     218            2 :     _assureInitialized();
     219              : 
     220            4 :     await _storage.setDomain(
     221            4 :       pairs.map((key, value) {
     222            4 :         final iv = CipherStorage.ivFromSecureRandom().base64;
     223              : 
     224            2 :         return MapEntry(
     225            4 :           _encryptHelper.encrypt(key),
     226            6 :           StorageValue(_encryptHelper.encrypt(value, iv), iv),
     227              :         );
     228              :       }),
     229              :       domain: domain,
     230              :       overwrite: overwrite,
     231              :     );
     232              : 
     233            2 :     await _notifyAllSubjects(null, domain: domain);
     234              :   }
     235              : 
     236            2 :   void _assureInitialized() {
     237            2 :     if (!_initialized) {
     238            0 :       throw Exception('TheStorage is not initialized!');
     239              :     }
     240              :   }
     241              : 
     242            1 :   @override
     243              :   Future<ValueStream<String?>> subscribe(
     244              :     String key, {
     245              :     String? defaultValue,
     246              :     String domain = AbstractStorage.defaultDomain,
     247              :     bool keepAlive = true,
     248              :   }) async {
     249            1 :     _assureInitialized();
     250              : 
     251            2 :     final subject = _subjects[(key, domain, defaultValue, keepAlive)];
     252              :     if (subject != null) {
     253            1 :       return subject.stream;
     254              :     }
     255              : 
     256            1 :     final data = await get(key, defaultValue: defaultValue, domain: domain);
     257              : 
     258            1 :     final newSubject = BehaviorSubject<String?>.seeded(
     259              :       data,
     260              :       onCancel: keepAlive
     261              :           ? null
     262            1 :           : () {
     263            1 :               unawaited(
     264            3 :                 _subjects[(key, domain, defaultValue, keepAlive)]?.close(),
     265              :               );
     266            2 :               _subjects.remove((key, domain, defaultValue, keepAlive));
     267              :             },
     268              :     );
     269            2 :     _subjects[(key, domain, defaultValue, keepAlive)] = newSubject;
     270              : 
     271            1 :     return newSubject.stream;
     272              :   }
     273              : 
     274            1 :   @override
     275              :   Future<ValueStream<Map<String, String>>> subscribeDomain({
     276              :     String domain = AbstractStorage.defaultDomain,
     277              :     bool keepAlive = true,
     278              :   }) async {
     279            1 :     _assureInitialized();
     280              : 
     281            2 :     final subject = _domainSubjects[(domain, keepAlive)];
     282              :     if (subject != null) {
     283            1 :       return subject.stream;
     284              :     }
     285              : 
     286            1 :     final data = await getDomain(domain: domain);
     287              : 
     288            1 :     final newSubject = BehaviorSubject<Map<String, String>>.seeded(
     289              :       data,
     290              :       onCancel: keepAlive
     291              :           ? null
     292            1 :           : () {
     293            1 :               unawaited(
     294            3 :                 _domainSubjects[(domain, keepAlive)]?.close(),
     295              :               );
     296            2 :               _domainSubjects.remove((domain, keepAlive));
     297              :             },
     298              :     );
     299            2 :     _domainSubjects[(domain, keepAlive)] = newSubject;
     300              : 
     301            1 :     return newSubject.stream;
     302              :   }
     303              : 
     304            1 :   @override
     305              :   Future<ValueStream<List<String>>> subscribeDomainKeys({
     306              :     String domain = AbstractStorage.defaultDomain,
     307              :     bool keepAlive = true,
     308              :   }) async {
     309            1 :     _assureInitialized();
     310              : 
     311            2 :     final subject = _domainKeysSubjects[(domain, keepAlive)];
     312              :     if (subject != null) {
     313            1 :       return subject.stream;
     314              :     }
     315              : 
     316            1 :     final data = await getDomainKeys(domain: domain);
     317              : 
     318            1 :     final newSubject = BehaviorSubject<List<String>>.seeded(
     319              :       data,
     320              :       onCancel: keepAlive
     321              :           ? null
     322            1 :           : () {
     323            1 :               unawaited(
     324            3 :                 _domainKeysSubjects[(domain, keepAlive)]?.close(),
     325              :               );
     326            2 :               _domainKeysSubjects.remove((domain, keepAlive));
     327              :             },
     328              :     );
     329            2 :     _domainKeysSubjects[(domain, keepAlive)] = newSubject;
     330              : 
     331            1 :     return newSubject.stream;
     332              :   }
     333              : 
     334            2 :   void _clearSubjects() {
     335            5 :     for (final subject in _subjects.values) {
     336            2 :       unawaited(subject.close());
     337              :     }
     338            4 :     _subjects.clear();
     339              : 
     340            5 :     for (final subject in _domainSubjects.values) {
     341            2 :       unawaited(subject.close());
     342              :     }
     343            4 :     _domainSubjects.clear();
     344              : 
     345            5 :     for (final subject in _domainKeysSubjects.values) {
     346            2 :       unawaited(subject.close());
     347              :     }
     348            4 :     _domainKeysSubjects.clear();
     349              :   }
     350              : 
     351            2 :   void _notifyClearAll() {
     352            5 :     for (final subject in _subjects.values) {
     353            1 :       subject.add(null);
     354              :     }
     355              : 
     356            5 :     for (final subject in _domainSubjects.values) {
     357            2 :       subject.add({});
     358              :     }
     359              : 
     360            5 :     for (final subject in _domainKeysSubjects.values) {
     361            2 :       subject.add([]);
     362              :     }
     363              :   }
     364              : 
     365            2 :   Future<void> _notifyAllSubjects(
     366              :     String? key, {
     367              :     required String domain,
     368              :   }) async {
     369            4 :     await Future.wait([
     370            2 :       _notifySubjects(key, domain: domain),
     371            2 :       _notifyDomainSubjects(domain),
     372            2 :       _notifyDomainKeysSubjects(domain),
     373              :     ]);
     374              :   }
     375              : 
     376              :   /// Notify all [_subjects] filtered by [key] and [domain]. If [key] is null
     377              :   /// then all [_subjects] with [domain] will be notified.
     378            2 :   Future<void> _notifySubjects(
     379              :     String? key, {
     380              :     required String domain,
     381              :   }) async {
     382            4 :     if (_subjects.isEmpty) {
     383              :       return;
     384              :     }
     385              : 
     386              :     if (key == null) {
     387            1 :       final domainPairs = await getDomain(domain: domain);
     388            3 :       for (final entry in _subjects.entries.where(
     389            3 :         (entry) => entry.key.$2 == domain,
     390            1 :       )) {
     391            4 :         entry.value.add(domainPairs[entry.key.$1]);
     392              :       }
     393              :       return;
     394              :     }
     395              : 
     396            1 :     final value = await get(key, domain: domain);
     397            3 :     for (final entry in _subjects.entries.where(
     398            5 :       (entry) => entry.key.$1 == key && entry.key.$2 == domain,
     399            1 :     )) {
     400            2 :       entry.value.add(value);
     401              :     }
     402              :   }
     403              : 
     404              :   /// Notify all [_domainSubjects] filtered by [domain].
     405            2 :   Future<void> _notifyDomainSubjects(String domain) async {
     406            4 :     if (_domainSubjects.isEmpty) {
     407              :       return;
     408              :     }
     409              : 
     410            1 :     final domainPairs = await getDomain(domain: domain);
     411            3 :     for (final entry in _domainSubjects.entries.where(
     412            3 :       (entry) => entry.key.$1 == domain,
     413            1 :     )) {
     414            2 :       entry.value.add(domainPairs);
     415              :     }
     416              :   }
     417              : 
     418              :   /// Notify all [_domainKeysSubjects] filtered by [domain].
     419            2 :   Future<void> _notifyDomainKeysSubjects(String domain) async {
     420            4 :     if (_domainKeysSubjects.isEmpty) {
     421              :       return;
     422              :     }
     423              : 
     424            1 :     final domainKeys = await getDomainKeys(domain: domain);
     425            3 :     for (final entry in _domainKeysSubjects.entries.where(
     426            3 :       (entry) => entry.key.$1 == domain,
     427            1 :     )) {
     428            2 :       entry.value.add(domainKeys);
     429              :     }
     430              :   }
     431              : }
        

Generated by: LCOV version 2.4-0