Line data Source code
1 : part of flutter_data;
2 :
3 : class DataGraphNotifier extends StateNotifier<DataGraphEvent>
4 : with _Lifecycle<DataGraphNotifier> {
5 1 : @protected
6 1 : DataGraphNotifier(this._hiveLocalStorage) : super(null);
7 :
8 : final HiveLocalStorage _hiveLocalStorage;
9 :
10 : @protected
11 : Box<Map> box;
12 : bool _doAssert = true;
13 :
14 : @override
15 1 : Future<DataGraphNotifier> initialize() async {
16 1 : if (isInitialized) return this;
17 3 : await _hiveLocalStorage.initialize();
18 5 : box = await _hiveLocalStorage.hive.openBox('_graph');
19 :
20 2 : await super.initialize();
21 : return this;
22 : }
23 :
24 1 : @override
25 1 : bool get isInitialized => box != null;
26 :
27 : @override
28 1 : Future<void> dispose() async {
29 2 : await super.dispose();
30 : }
31 :
32 : // key-related methods
33 :
34 1 : String getKeyForId(String type, dynamic id, {String keyIfAbsent}) {
35 1 : type = DataHelpers.getType(type);
36 : if (id != null) {
37 1 : final _id = 'id:$type#$id';
38 :
39 1 : if (_getNode(_id) != null) {
40 1 : final tos = _getEdge(_id, metadata: 'key');
41 1 : if (tos != null && tos.isNotEmpty) {
42 1 : final key = tos.first;
43 : return key;
44 : }
45 : }
46 :
47 : if (keyIfAbsent != null) {
48 : // this means the method is instructed to
49 : // create nodes and edges
50 :
51 1 : if (!_hasNode(keyIfAbsent)) {
52 1 : _addNode(keyIfAbsent, notify: false);
53 : }
54 1 : if (!_hasNode(_id)) {
55 1 : _addNode(_id, notify: false);
56 : }
57 1 : _removeEdges(keyIfAbsent,
58 : metadata: 'id', inverseMetadata: 'key', notify: false);
59 1 : _addEdge(keyIfAbsent, _id,
60 : metadata: 'id', inverseMetadata: 'key', notify: false);
61 : return keyIfAbsent;
62 : }
63 : } else if (keyIfAbsent != null) {
64 : // if no ID is supplied but keyIfAbsent is, create node for key
65 1 : if (!_hasNode(keyIfAbsent)) {
66 1 : _addNode(keyIfAbsent, notify: false);
67 : }
68 : return keyIfAbsent;
69 : }
70 : return null;
71 : }
72 :
73 2 : void removeKey(String key) => _removeNode(key);
74 :
75 : // id-related methods
76 :
77 1 : String getId(String key) {
78 1 : final tos = _getEdge(key, metadata: 'id');
79 1 : return tos == null || tos.isEmpty
80 : ? null
81 5 : : (denamespace(tos.first).split('#')..removeAt(0)).join('#');
82 : }
83 :
84 0 : void removeId(String type, dynamic id) => _removeNode('id:$type#$id');
85 :
86 : // nodes
87 :
88 1 : void _assertKey(String key) {
89 1 : if (_doAssert && key != null) {
90 4 : assert(key.split(':').length == 2);
91 : }
92 : }
93 :
94 1 : void addNode(String key, {bool notify = true}) {
95 1 : _assertKey(key);
96 1 : _addNode(key, notify: notify);
97 : }
98 :
99 1 : void addNodes(Iterable<String> keys, {bool notify = true}) {
100 2 : for (final key in keys) {
101 1 : _assertKey(key);
102 : }
103 1 : _addNodes(keys, notify: notify);
104 : }
105 :
106 1 : Map<String, List<String>> getNode(String key) {
107 1 : _assertKey(key);
108 1 : return _getNode(key);
109 : }
110 :
111 1 : bool hasNode(String key) {
112 1 : _assertKey(key);
113 1 : return _hasNode(key);
114 : }
115 :
116 1 : void removeNode(String key) {
117 1 : _assertKey(key);
118 1 : return _removeNode(key);
119 : }
120 :
121 : // edges
122 :
123 1 : void addEdges(String from,
124 : {@required String metadata,
125 : @required Iterable<String> tos,
126 : String inverseMetadata,
127 : bool notify = true}) {
128 1 : _assertKey(from);
129 1 : _assertKey(metadata);
130 1 : _assertKey(inverseMetadata);
131 1 : _addEdges(from,
132 : metadata: metadata, tos: tos, inverseMetadata: inverseMetadata);
133 : }
134 :
135 1 : List<String> getEdge(String key, {@required String metadata}) {
136 1 : _assertKey(key);
137 1 : _assertKey(metadata);
138 1 : return _getEdge(key, metadata: metadata);
139 : }
140 :
141 1 : void addEdge(String from, String to,
142 : {@required String metadata, String inverseMetadata, bool notify = true}) {
143 1 : _assertKey(from);
144 1 : _assertKey(metadata);
145 1 : _assertKey(inverseMetadata);
146 1 : return _addEdge(from, to,
147 : metadata: metadata, inverseMetadata: inverseMetadata, notify: notify);
148 : }
149 :
150 1 : void removeEdges(String from,
151 : {@required String metadata,
152 : Iterable<String> tos,
153 : String inverseMetadata,
154 : bool notify = true}) {
155 1 : _assertKey(from);
156 1 : _assertKey(metadata);
157 1 : _assertKey(inverseMetadata);
158 1 : return _removeEdges(from,
159 : metadata: metadata, inverseMetadata: inverseMetadata, notify: notify);
160 : }
161 :
162 1 : void removeEdge(String from, String to,
163 : {@required String metadata, String inverseMetadata, bool notify = true}) {
164 1 : _assertKey(from);
165 1 : _assertKey(metadata);
166 1 : _assertKey(inverseMetadata);
167 1 : return _removeEdge(from, to,
168 : metadata: metadata, inverseMetadata: inverseMetadata, notify: notify);
169 : }
170 :
171 1 : bool hasEdge(String key, {@required String metadata}) {
172 1 : _assertKey(key);
173 1 : _assertKey(metadata);
174 1 : return _hasEdge(key, metadata: metadata);
175 : }
176 :
177 3 : String denamespace(String namespacedKey) => namespacedKey.split(':').last;
178 :
179 : // debug utilities
180 :
181 2 : Map<String, Map> dumpGraph() => _toMap();
182 :
183 1 : @protected
184 : @visibleForTesting
185 1 : void debugAssert(bool value) => _doAssert = value;
186 :
187 : // private API
188 :
189 1 : Map<String, List<String>> _getNode(String key) {
190 0 : assert(key != null, 'key cannot be null');
191 3 : return box.get(key)?.cast<String, List<String>>();
192 : }
193 :
194 1 : bool _hasNode(String key) {
195 2 : return box.containsKey(key);
196 : }
197 :
198 1 : List<String> _getEdge(String key, {@required String metadata}) {
199 1 : final node = _getNode(key);
200 : if (node != null) {
201 1 : return node[metadata];
202 : }
203 : return null;
204 : }
205 :
206 1 : bool _hasEdge(String key, {@required String metadata}) {
207 1 : final fromNode = _getNode(key);
208 2 : return fromNode?.keys?.contains(metadata) ?? false;
209 : }
210 :
211 : // write
212 :
213 1 : void _addNodes(Iterable<String> keys, {bool notify = true}) {
214 2 : for (final key in keys) {
215 1 : _addNode(key, notify: notify);
216 : }
217 : }
218 :
219 1 : void _addNode(String key, {bool notify = true}) {
220 0 : assert(key != null, 'key cannot be null');
221 2 : if (!box.containsKey(key)) {
222 3 : box.put(key, {});
223 : if (notify) {
224 2 : state = DataGraphEvent(
225 1 : keys: [key], type: DataGraphEventType.addNode, graph: this);
226 : }
227 : }
228 : }
229 :
230 1 : void _removeNode(String key, {bool notify = true}) {
231 0 : assert(key != null, 'key cannot be null');
232 1 : final fromNode = _getNode(key);
233 :
234 : if (fromNode == null) {
235 : return;
236 : }
237 :
238 : // sever all incoming edges
239 2 : for (final toKey in _connectedKeys(key)) {
240 1 : final toNode = _getNode(toKey);
241 : // remove deleted key from all metadatas
242 : if (toNode != null) {
243 3 : for (final entry in toNode.entries.toSet()) {
244 3 : _removeEdges(toKey, tos: [key], metadata: entry.key);
245 : }
246 : }
247 : }
248 :
249 2 : box.delete(key);
250 :
251 : if (notify) {
252 2 : state = DataGraphEvent(
253 1 : keys: [key], type: DataGraphEventType.removeNode, graph: this);
254 : }
255 : }
256 :
257 1 : void _addEdge(String from, String to,
258 : {@required String metadata, String inverseMetadata, bool notify = true}) {
259 1 : _addEdges(from,
260 1 : tos: [to],
261 : metadata: metadata,
262 : inverseMetadata: inverseMetadata,
263 : notify: notify);
264 : }
265 :
266 1 : void _addEdges(String from,
267 : {@required String metadata,
268 : @required Iterable<String> tos,
269 : String inverseMetadata,
270 : bool notify = true}) {
271 1 : final fromNode = _getNode(from);
272 0 : assert(fromNode != null && tos != null);
273 :
274 1 : if (tos.isEmpty) {
275 : return;
276 : }
277 :
278 : // use a set to ensure resulting list elements are unique
279 5 : fromNode[metadata] = {...?fromNode[metadata], ...tos}.toList();
280 :
281 : if (notify) {
282 2 : state = DataGraphEvent(
283 4 : keys: [from, ...tos],
284 : metadata: metadata,
285 : type: DataGraphEventType.addEdge,
286 : graph: this,
287 : );
288 : }
289 :
290 : if (inverseMetadata != null) {
291 2 : for (final to in tos) {
292 : // get or create toNode
293 : final toNode =
294 2 : _hasNode(to) ? _getNode(to) : (this.._addNode(to))._getNode(to);
295 :
296 : // use a set to ensure resulting list elements are unique
297 6 : toNode[inverseMetadata] = {...?toNode[inverseMetadata], from}.toList();
298 : }
299 :
300 : if (notify) {
301 2 : state = DataGraphEvent(
302 4 : keys: [...tos, from],
303 : metadata: inverseMetadata,
304 : type: DataGraphEventType.addEdge,
305 : graph: this,
306 : );
307 : }
308 : }
309 : }
310 :
311 1 : void _removeEdge(String from, String to,
312 : {@required String metadata, String inverseMetadata, bool notify = true}) {
313 1 : _removeEdges(from,
314 1 : tos: [to],
315 : metadata: metadata,
316 : inverseMetadata: inverseMetadata,
317 : notify: notify);
318 : }
319 :
320 1 : void _removeEdges(String from,
321 : {@required String metadata,
322 : Iterable<String> tos,
323 : String inverseMetadata,
324 : bool notify = true}) {
325 1 : final fromNode = _getNode(from);
326 0 : assert(fromNode != null);
327 :
328 1 : if (tos != null && fromNode[metadata] != null) {
329 : // remove all tos from fromNode[metadata]
330 3 : fromNode[metadata].removeWhere(tos.contains);
331 2 : if (fromNode[metadata].isEmpty) {
332 1 : fromNode.remove(metadata);
333 : }
334 : } else {
335 : // tos == null as argument means ALL
336 : // remove metadata and retrieve all tos
337 1 : tos = fromNode.remove(metadata);
338 : }
339 :
340 : if (notify) {
341 2 : state = DataGraphEvent(
342 4 : keys: [from, ...?tos],
343 : metadata: metadata,
344 : type: DataGraphEventType.removeEdge,
345 : graph: this,
346 : );
347 : }
348 :
349 : if (tos != null && inverseMetadata != null) {
350 2 : for (final to in tos) {
351 1 : final toNode = _getNode(to);
352 1 : if (toNode != null && toNode[inverseMetadata] != null) {
353 2 : toNode[inverseMetadata].remove(from);
354 2 : if (toNode[inverseMetadata].isEmpty) {
355 1 : toNode.remove(inverseMetadata);
356 : }
357 : }
358 1 : if (toNode.isEmpty) {
359 1 : _removeNode(to, notify: notify);
360 : }
361 : }
362 :
363 : if (notify) {
364 2 : state = DataGraphEvent(
365 4 : keys: [...tos, from],
366 : metadata: inverseMetadata,
367 : type: DataGraphEventType.removeEdge,
368 : graph: this,
369 : );
370 : }
371 : }
372 : }
373 :
374 1 : void _notify(List<String> keys, DataGraphEventType type) {
375 2 : state = DataGraphEvent(
376 : type: type,
377 : keys: keys,
378 : graph: this,
379 : );
380 : }
381 :
382 : // misc
383 :
384 1 : Set<String> _connectedKeys(String key, {Iterable<String> metadatas}) {
385 1 : final node = _getNode(key);
386 : if (node == null) {
387 : return {};
388 : }
389 :
390 3 : return node.entries.fold({}, (acc, entry) {
391 0 : if (metadatas != null && !metadatas.contains(entry.key)) {
392 : return acc;
393 : }
394 2 : return acc..addAll(entry.value);
395 : });
396 : }
397 :
398 4 : Map<String, Map> _toMap() => box.toMap().cast();
399 : }
400 :
401 1 : enum DataGraphEventType {
402 1 : addNode,
403 1 : removeNode,
404 1 : updateNode,
405 1 : addEdge,
406 1 : removeEdge,
407 1 : updateEdge
408 : }
409 :
410 : extension DataGraphEventTypeX on DataGraphEventType {
411 2 : bool get isNode => [
412 : DataGraphEventType.addNode,
413 : DataGraphEventType.updateNode,
414 : DataGraphEventType.removeNode,
415 1 : ].contains(this);
416 2 : bool get isEdge => [
417 : DataGraphEventType.addEdge,
418 : DataGraphEventType.updateEdge,
419 : DataGraphEventType.removeEdge,
420 1 : ].contains(this);
421 : }
422 :
423 : class DataGraphEvent {
424 1 : const DataGraphEvent({
425 : this.keys,
426 : this.metadata,
427 : this.type,
428 : this.graph,
429 : });
430 : final List<String> keys;
431 : final String metadata;
432 : final DataGraphEventType type;
433 : final DataGraphNotifier graph;
434 :
435 0 : @override
436 : String toString() {
437 0 : return '[GraphEvent] $type: $keys';
438 : }
439 : }
440 :
441 3 : final graphProvider = Provider<DataGraphNotifier>((ref) {
442 0 : return DataGraphNotifier(ref.read(hiveLocalStorageProvider));
443 : });
|