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

Generated by: LCOV version 1.15