LCOV - code coverage report
Current view: top level - src/graph - graph_notifier.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 172 175 98.3 %
Date: 2020-07-30 22:52:57 Functions: 0 0 -

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

Generated by: LCOV version 1.14