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