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