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

Generated by: LCOV version 1.15