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-03-22 12:33:26 Functions: - 0 0

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

Generated by: LCOV version 2.4-0