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