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 : });
|