LCOV - code coverage report
Current view: top level - graph - graph_notifier.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 180 184 97.8 %
Date: 2022-06-06 11:59:57 Functions: 0 0 -

          Line data    Source code
       1             : part of flutter_data;
       2             : 
       3             : const _kGraphBoxName = '_graph';
       4             : 
       5             : /// A bidirected graph data structure that notifies
       6             : /// modification events through a [StateNotifier].
       7             : ///
       8             : /// It's a core framework component as it holds all
       9             : /// relationship information.
      10             : ///
      11             : /// Watchers like [Repository.watchAllNotifier] or [BelongsTo.watch]
      12             : /// make use of it.
      13             : ///
      14             : /// Its public API requires all keys and metadata to be namespaced
      15             : /// i.e. `manager:key`
      16             : class GraphNotifier extends DelayedStateNotifier<DataGraphEvent>
      17             :     with _Lifecycle {
      18             :   final Reader read;
      19          11 :   @protected
      20             :   GraphNotifier(this.read);
      21             : 
      22          44 :   HiveLocalStorage get _hiveLocalStorage => read(hiveLocalStorageProvider);
      23             : 
      24             :   @protected
      25             :   Box<Map>? box;
      26             :   bool _doAssert = true;
      27             : 
      28             :   /// Initializes Hive local storage and box it depends on
      29          11 :   Future<GraphNotifier> initialize() async {
      30          11 :     if (isInitialized) return this;
      31          33 :     await _hiveLocalStorage.initialize();
      32          22 :     if (_hiveLocalStorage.clear) {
      33           0 :       await _hiveLocalStorage.deleteBox(_kGraphBoxName);
      34             :     }
      35          44 :     box = await _hiveLocalStorage.openBox(_kGraphBoxName);
      36             : 
      37             :     return this;
      38             :   }
      39             : 
      40          10 :   @override
      41             :   void dispose() {
      42          10 :     if (isInitialized) {
      43          20 :       box?.close();
      44          10 :       super.dispose();
      45             :     }
      46             :   }
      47             : 
      48           1 :   Future<void> clear() async {
      49           3 :     await box?.clear();
      50             :   }
      51             : 
      52          11 :   @override
      53          21 :   bool get isInitialized => box?.isOpen ?? false;
      54             : 
      55             :   // key-related methods
      56             : 
      57             :   /// Finds a model's key in the graph.
      58             :   ///
      59             :   ///  - Attempts a lookup by [type]/[id]
      60             :   ///  - If the key was not found, it returns a default [keyIfAbsent]
      61             :   ///    (if provided)
      62             :   ///  - It associates [keyIfAbsent] with the supplied [type]/[id]
      63             :   ///    (if both [keyIfAbsent] & [type]/[id] were provided)
      64          11 :   String? getKeyForId(String type, Object? id, {String? keyIfAbsent}) {
      65          11 :     type = DataHelpers.getType(type);
      66             :     if (id != null) {
      67          11 :       final namespace = id is int ? '_id_int' : '_id';
      68             :       final namespacedId =
      69          33 :           id.toString().typifyWith(type).namespaceWith(namespace);
      70             : 
      71          11 :       if (_getNode(namespacedId) != null) {
      72          11 :         final tos = _getEdge(namespacedId, metadata: '_key');
      73          11 :         if (tos.isNotEmpty) {
      74          11 :           final key = tos.first;
      75             :           return key;
      76             :         }
      77             :       }
      78             : 
      79             :       if (keyIfAbsent != null) {
      80             :         // this means the method is instructed to
      81             :         // create nodes and edges
      82             : 
      83          11 :         _addNode(keyIfAbsent, notify: false);
      84          11 :         _addNode(namespacedId, notify: false);
      85          11 :         _removeEdges(keyIfAbsent,
      86             :             metadata: '_id', inverseMetadata: '_key', notify: false);
      87          11 :         _addEdge(keyIfAbsent, namespacedId,
      88             :             metadata: '_id', inverseMetadata: '_key', notify: false);
      89             :         return keyIfAbsent;
      90             :       }
      91             :     } else if (keyIfAbsent != null) {
      92             :       // if no ID is supplied but keyIfAbsent is, create node for key
      93          11 :       _addNode(keyIfAbsent, notify: false);
      94             :       return keyIfAbsent;
      95             :     }
      96             :     return null;
      97             :   }
      98             : 
      99             :   /// Removes key (and its edges) from graph
     100          12 :   void removeKey(String key) => _removeNode(key);
     101             : 
     102             :   /// Finds an ID in the graph, given a [key].
     103           6 :   Object? getIdForKey(String key) {
     104           6 :     final tos = _getEdge(key, metadata: '_id');
     105           6 :     if (tos.isEmpty) {
     106             :       return null;
     107             :     }
     108          18 :     final isInt = tos.first.namespace == '_id_int';
     109          18 :     final id = tos.first.denamespace().detypify();
     110           4 :     return isInt ? int.tryParse(id) : id;
     111             :   }
     112             : 
     113             :   /// Removes [type]/[id] (and its edges) from graph
     114           2 :   void removeId(String type, Object id, {bool notify = true}) =>
     115           8 :       _removeNode(id.toString().typifyWith(type).namespaceWith('_id'),
     116             :           notify: notify);
     117             : 
     118             :   // nodes
     119             : 
     120           1 :   void _assertKey(String key) {
     121           1 :     if (_doAssert) {
     122           4 :       if (key.split(':').length != 2 || key.startsWith('_')) {
     123           1 :         throw AssertionError('''
     124             : Key "$key":
     125             :   - Key must be namespaced (my:key)
     126             :   - Key can't contain a colon (my:precious:key)
     127             :   - Namespace can't start with an underscore (_my:key)
     128           1 : ''');
     129             :       }
     130             :     }
     131             :   }
     132             : 
     133             :   /// Adds a node, [key] MUST be namespaced (e.g. `manager:key`)
     134           1 :   void addNode(String key, {bool notify = true}) {
     135           1 :     _assertKey(key);
     136           1 :     _addNode(key, notify: notify);
     137             :   }
     138             : 
     139             :   /// Adds nodes, all [keys] MUST be namespaced (e.g. `manager:key`)
     140           1 :   void addNodes(Iterable<String> keys, {bool notify = true}) {
     141           2 :     for (final key in keys) {
     142           1 :       _assertKey(key);
     143             :     }
     144           1 :     _addNodes(keys, notify: notify);
     145             :   }
     146             : 
     147             :   /// Obtains a node
     148           2 :   Map<String, List<String>>? getNode(String key,
     149             :       {bool orAdd = false, bool notify = true}) {
     150           2 :     return _getNode(key, orAdd: orAdd, notify: notify);
     151             :   }
     152             : 
     153             :   /// Returns whether [key] is present in this graph.
     154             :   ///
     155             :   /// [key] MUST be namespaced (e.g. `manager:key`)
     156           1 :   bool hasNode(String key) {
     157           1 :     return _hasNode(key);
     158             :   }
     159             : 
     160             :   /// Removes a node, [key] MUST be namespaced (e.g. `manager:key`)
     161           1 :   void removeNode(String key) {
     162           1 :     _assertKey(key);
     163           1 :     return _removeNode(key);
     164             :   }
     165             : 
     166             :   // edges
     167             : 
     168             :   /// See [addEdge]
     169           1 :   void addEdges(String from,
     170             :       {required String metadata,
     171             :       required Iterable<String> tos,
     172             :       String? inverseMetadata,
     173             :       bool addNode = false,
     174             :       bool notify = true}) {
     175           1 :     _assertKey(from);
     176           2 :     for (final to in tos) {
     177           1 :       _assertKey(to);
     178             :     }
     179           1 :     _assertKey(metadata);
     180             :     if (inverseMetadata != null) {
     181           1 :       _assertKey(inverseMetadata);
     182             :     }
     183           1 :     _addEdges(from,
     184             :         metadata: metadata,
     185             :         tos: tos,
     186             :         addNode: addNode,
     187             :         inverseMetadata: inverseMetadata);
     188             :   }
     189             : 
     190             :   /// Returns edge by [metadata]
     191             :   ///
     192             :   /// [key] and [metadata] MUST be namespaced (e.g. `manager:key`)
     193           1 :   List<String> getEdge(String key, {required String metadata}) {
     194           1 :     _assertKey(key);
     195           1 :     _assertKey(metadata);
     196           1 :     return _getEdge(key, metadata: metadata);
     197             :   }
     198             : 
     199             :   /// Adds a bidirectional edge:
     200             :   ///
     201             :   ///  - [from]->[to] with [metadata]
     202             :   ///  - [to]->[from] with [inverseMetadata]
     203             :   ///
     204             :   /// [from], [metadata] & [inverseMetadata] MUST be namespaced (e.g. `manager:key`)
     205           1 :   void addEdge(String from, String to,
     206             :       {required String metadata,
     207             :       String? inverseMetadata,
     208             :       bool addNode = false,
     209             :       bool notify = true}) {
     210           1 :     _assertKey(from);
     211           1 :     _assertKey(to);
     212           1 :     _assertKey(metadata);
     213             :     if (inverseMetadata != null) {
     214           1 :       _assertKey(inverseMetadata);
     215             :     }
     216           1 :     return _addEdge(from, to,
     217             :         metadata: metadata,
     218             :         inverseMetadata: inverseMetadata,
     219             :         addNode: addNode,
     220             :         notify: notify);
     221             :   }
     222             : 
     223             :   /// See [removeEdge]
     224           1 :   void removeEdges(String from,
     225             :       {required String metadata,
     226             :       Iterable<String> tos = const [],
     227             :       String? inverseMetadata,
     228             :       bool notify = true}) {
     229           1 :     _assertKey(from);
     230           1 :     for (final to in tos) {
     231           0 :       _assertKey(to);
     232             :     }
     233           1 :     _assertKey(metadata);
     234             :     if (inverseMetadata != null) {
     235           0 :       _assertKey(inverseMetadata);
     236             :     }
     237           1 :     return _removeEdges(from,
     238             :         metadata: metadata, inverseMetadata: inverseMetadata, notify: notify);
     239             :   }
     240             : 
     241             :   /// Removes a bidirectional edge:
     242             :   ///
     243             :   ///  - [from]->[to] with [metadata]
     244             :   ///  - [to]->[from] with [inverseMetadata]
     245             :   ///
     246             :   /// [from], [metadata] & [inverseMetadata] MUST be namespaced (e.g. `manager:key`)
     247           1 :   void removeEdge(String from, String to,
     248             :       {required String metadata, String? inverseMetadata, bool notify = true}) {
     249           1 :     _assertKey(from);
     250           1 :     _assertKey(to);
     251           1 :     _assertKey(metadata);
     252             :     if (inverseMetadata != null) {
     253           1 :       _assertKey(inverseMetadata);
     254             :     }
     255           1 :     return _removeEdge(from, to,
     256             :         metadata: metadata, inverseMetadata: inverseMetadata, notify: notify);
     257             :   }
     258             : 
     259             :   /// Returns whether the requested edge is present in this graph.
     260             :   ///
     261             :   /// [key] and [metadata] MUST be namespaced (e.g. `manager:key`)
     262           1 :   bool hasEdge(String key, {required String metadata}) {
     263           1 :     _assertKey(key);
     264           1 :     _assertKey(metadata);
     265           1 :     return _hasEdge(key, metadata: metadata);
     266             :   }
     267             : 
     268             :   /// Removes orphan nodes (i.e. nodes without edges)
     269          11 :   @protected
     270             :   @visibleForTesting
     271             :   void removeOrphanNodes() {
     272          77 :     final orphanEntries = {...toMap()}.entries.where((e) => e.value.isEmpty);
     273          12 :     for (final e in orphanEntries) {
     274           2 :       _removeNode(e.key);
     275             :     }
     276             :   }
     277             : 
     278             :   // utils
     279             : 
     280             :   /// Returns a [Map] representation of this graph, the underlying Hive [box].
     281          22 :   Map<String, Map> toMap() => _toMap();
     282             : 
     283          11 :   @protected
     284             :   @visibleForTesting
     285          11 :   void debugAssert(bool value) => _doAssert = value;
     286             : 
     287             :   // private API
     288             : 
     289          11 :   Map<String, List<String>>? _getNode(String key,
     290             :       {bool orAdd = false, bool notify = true}) {
     291          11 :     if (orAdd) _addNode(key, notify: notify);
     292          33 :     return box?.get(key)?.cast<String, List<String>>();
     293             :   }
     294             : 
     295          11 :   bool _hasNode(String key) {
     296          22 :     return box?.containsKey(key) ?? false;
     297             :   }
     298             : 
     299          11 :   List<String> _getEdge(String key, {required String metadata}) {
     300          11 :     final node = _getNode(key);
     301             :     if (node != null) {
     302          22 :       return node[metadata] ?? [];
     303             :     }
     304           1 :     return [];
     305             :   }
     306             : 
     307           6 :   bool _hasEdge(String key, {required String metadata}) {
     308           6 :     final fromNode = _getNode(key);
     309          12 :     return fromNode?.keys.contains(metadata) ?? false;
     310             :   }
     311             : 
     312             :   // write
     313             : 
     314           1 :   void _addNodes(Iterable<String> keys, {bool notify = true}) {
     315           2 :     for (final key in keys) {
     316           1 :       _addNode(key, notify: notify);
     317             :     }
     318             :   }
     319             : 
     320          11 :   void _addNode(String key, {bool notify = true}) {
     321          11 :     if (!_hasNode(key)) {
     322          33 :       box?.put(key, {});
     323             :       if (notify) {
     324           6 :         state = DataGraphEvent(keys: [key], type: DataGraphEventType.addNode);
     325             :       }
     326             :     }
     327             :   }
     328             : 
     329           8 :   void _removeNode(String key, {bool notify = true}) {
     330           8 :     final fromNode = _getNode(key);
     331             : 
     332             :     if (fromNode == null) {
     333             :       return;
     334             :     }
     335             : 
     336             :     // sever all incoming edges
     337          14 :     for (final toKey in _connectedKeys(key)) {
     338           6 :       final toNode = _getNode(toKey);
     339             :       // remove deleted key from all metadatas
     340             :       if (toNode != null) {
     341          18 :         for (final entry in toNode.entries.toSet()) {
     342          12 :           _removeEdge(toKey, key, metadata: entry.key, notify: notify);
     343             :         }
     344             :       }
     345             :     }
     346             : 
     347          16 :     box?.delete(key);
     348             : 
     349             :     if (notify) {
     350          18 :       state = DataGraphEvent(keys: [key], type: DataGraphEventType.removeNode);
     351             :     }
     352             :   }
     353             : 
     354          11 :   void _addEdge(String from, String to,
     355             :       {required String metadata,
     356             :       String? inverseMetadata,
     357             :       bool addNode = false,
     358             :       bool notify = true}) {
     359          11 :     _addEdges(from,
     360          11 :         tos: [to],
     361             :         metadata: metadata,
     362             :         inverseMetadata: inverseMetadata,
     363             :         addNode: addNode,
     364             :         notify: notify);
     365             :   }
     366             : 
     367          11 :   void _addEdges(String from,
     368             :       {required String metadata,
     369             :       required Iterable<String> tos,
     370             :       String? inverseMetadata,
     371             :       bool addNode = false,
     372             :       bool notify = true}) {
     373          11 :     final fromNode = _getNode(from, orAdd: addNode, notify: notify)!;
     374             : 
     375          11 :     if (tos.isEmpty) {
     376             :       return;
     377             :     }
     378             : 
     379             :     // use a set to ensure resulting list elements are unique
     380          44 :     fromNode[metadata] = {...?fromNode[metadata], ...tos}.toList();
     381             :     // persist change
     382          22 :     box?.put(from, fromNode);
     383             : 
     384             :     if (notify) {
     385           4 :       state = DataGraphEvent(
     386           4 :         keys: [from, ...tos],
     387             :         metadata: metadata,
     388             :         type: DataGraphEventType.addEdge,
     389             :       );
     390             :     }
     391             : 
     392             :     if (inverseMetadata != null) {
     393          22 :       for (final to in tos) {
     394             :         // get or create toNode
     395          11 :         final toNode = _getNode(to, orAdd: true, notify: notify)!;
     396             : 
     397             :         // use a set to ensure resulting list elements are unique
     398          44 :         toNode[inverseMetadata] = {...?toNode[inverseMetadata], from}.toList();
     399             :         // persist change
     400          22 :         box?.put(to, toNode);
     401             :       }
     402             :     }
     403             :   }
     404             : 
     405           9 :   void _removeEdge(String from, String to,
     406             :       {required String metadata, String? inverseMetadata, bool notify = true}) {
     407           9 :     _removeEdges(from,
     408           9 :         tos: [to],
     409             :         metadata: metadata,
     410             :         inverseMetadata: inverseMetadata,
     411             :         notify: notify);
     412             :   }
     413             : 
     414          11 :   void _removeEdges(String from,
     415             :       {required String metadata,
     416             :       Iterable<String>? tos,
     417             :       String? inverseMetadata,
     418             :       bool notify = true}) {
     419          11 :     if (!_hasNode(from)) return;
     420             : 
     421          11 :     final fromNode = _getNode(from)!;
     422             : 
     423           9 :     if (tos != null && fromNode[metadata] != null) {
     424             :       // remove all tos from fromNode[metadata]
     425          27 :       fromNode[metadata]?.removeWhere(tos.contains);
     426          18 :       if (fromNode[metadata]?.isEmpty ?? false) {
     427           8 :         fromNode.remove(metadata);
     428             :       }
     429             :       // persist change
     430          18 :       box?.put(from, fromNode);
     431             :     } else {
     432             :       // tos == null as argument means ALL
     433             :       // remove metadata and retrieve all tos
     434             : 
     435          11 :       if (fromNode.containsKey(metadata)) {
     436           8 :         tos = fromNode.remove(metadata);
     437             :       }
     438             :       // persist change
     439          22 :       box?.put(from, fromNode);
     440             :     }
     441             : 
     442             :     if (notify) {
     443          12 :       state = DataGraphEvent(
     444          12 :         keys: [from, ...?tos],
     445             :         metadata: metadata,
     446             :         type: DataGraphEventType.removeEdge,
     447             :       );
     448             :     }
     449             : 
     450             :     if (tos != null) {
     451          20 :       for (final to in tos) {
     452          10 :         final toNode = _getNode(to);
     453             :         if (toNode != null &&
     454             :             inverseMetadata != null &&
     455           7 :             toNode[inverseMetadata] != null) {
     456          14 :           toNode[inverseMetadata]?.remove(from);
     457          14 :           if (toNode[inverseMetadata]?.isEmpty ?? false) {
     458           7 :             toNode.remove(inverseMetadata);
     459             :           }
     460             :           // persist change
     461          14 :           box?.put(to, toNode);
     462             :         }
     463          10 :         if (toNode == null || toNode.isEmpty) {
     464           5 :           _removeNode(to, notify: notify);
     465             :         }
     466             :       }
     467             :     }
     468             :   }
     469             : 
     470          11 :   void _notify(List<String> keys,
     471             :       {String? metadata, required DataGraphEventType type}) {
     472          11 :     if (mounted) {
     473          22 :       state = DataGraphEvent(type: type, metadata: metadata, keys: keys);
     474             :     }
     475             :   }
     476             : 
     477             :   // misc
     478             : 
     479           8 :   Set<String> _connectedKeys(String key, {Iterable<String>? metadatas}) {
     480           8 :     final node = _getNode(key);
     481             :     if (node == null) {
     482             :       return {};
     483             :     }
     484             : 
     485          22 :     return node.entries.fold({}, (acc, entry) {
     486           0 :       if (metadatas != null && !metadatas.contains(entry.key)) {
     487             :         return acc;
     488             :       }
     489          12 :       return acc..addAll(entry.value);
     490             :     });
     491             :   }
     492             : 
     493          44 :   Map<String, Map> _toMap() => box!.toMap().cast();
     494             : }
     495             : 
     496          15 : enum DataGraphEventType {
     497             :   addNode,
     498             :   removeNode,
     499             :   updateNode,
     500             :   addEdge,
     501             :   removeEdge,
     502             :   updateEdge,
     503             :   doneLoading,
     504             : }
     505             : 
     506             : extension DataGraphEventTypeX on DataGraphEventType {
     507           6 :   bool get isNode => [
     508             :         DataGraphEventType.addNode,
     509             :         DataGraphEventType.updateNode,
     510             :         DataGraphEventType.removeNode,
     511           3 :       ].contains(this);
     512           8 :   bool get isEdge => [
     513             :         DataGraphEventType.addEdge,
     514             :         DataGraphEventType.updateEdge,
     515             :         DataGraphEventType.removeEdge,
     516           4 :       ].contains(this);
     517             : }
     518             : 
     519             : class DataGraphEvent {
     520          11 :   const DataGraphEvent({
     521             :     required this.keys,
     522             :     required this.type,
     523             :     this.metadata,
     524             :   });
     525             :   final List<String> keys;
     526             :   final DataGraphEventType type;
     527             :   final String? metadata;
     528             : 
     529           1 :   @override
     530             :   String toString() {
     531           4 :     return '${type.toShortString()}: $keys';
     532             :   }
     533             : }
     534             : 
     535             : extension _DataGraphEventX on DataGraphEventType {
     536           4 :   String toShortString() => toString().split('.').last;
     537             : }
     538             : 
     539          22 : final graphNotifierProvider =
     540          44 :     Provider<GraphNotifier>((ref) => GraphNotifier(ref.read));

Generated by: LCOV version 1.15