Line data Source code
1 : part of flutter_data;
2 :
3 : mixin _RemoteAdapterWatch<T extends DataModel<T>> on _RemoteAdapter<T> {
4 0 : @protected
5 : @visibleForTesting
6 : Duration get throttleDuration =>
7 : const Duration(milliseconds: 16); // 1 frame at 60fps
8 :
9 1 : @protected
10 : @visibleForTesting
11 : StateNotifier<List<DataGraphEvent>> get throttledGraph =>
12 3 : graph.throttle(throttleDuration);
13 :
14 1 : DataStateNotifier<List<T>> watchAll(
15 : {final bool remote,
16 : final Map<String, dynamic> params,
17 : final Map<String, String> headers}) {
18 1 : _assertInit();
19 1 : final _notifier = DataStateNotifier<List<T>>(
20 2 : DataState(localAdapter
21 1 : .findAll()
22 3 : .map((m) => initializeModel(m, save: true))
23 1 : .toList()),
24 1 : reload: (notifier) async {
25 : try {
26 1 : final _future = findAll(
27 : params: params, headers: headers, remote: remote, init: true);
28 4 : notifier.data = notifier.data.copyWith(isLoading: true);
29 1 : await _future;
30 : } catch (error, stackTrace) {
31 : // we're only interested in notifying errors
32 : // as models will pop up via the graph notifier
33 4 : notifier.data = notifier.data.copyWith(
34 : isLoading: false,
35 : exception: error,
36 : stackTrace: stackTrace,
37 : );
38 : }
39 : },
40 : );
41 :
42 : // kick off
43 1 : _notifier.reload();
44 :
45 3 : final _graphNotifier = throttledGraph.forEach((events) {
46 1 : if (!_notifier.mounted) {
47 : return;
48 : }
49 :
50 : // filter by keys (that are not IDs)
51 2 : final filteredEvents = events.where((event) =>
52 2 : event.type.isNode &&
53 4 : event.keys.first.startsWith(type) &&
54 4 : !event.graph._hasEdge(event.keys.first, metadata: 'key'));
55 :
56 1 : if (filteredEvents.isEmpty) {
57 : return;
58 : }
59 :
60 3 : final list = _notifier.data.model.toList();
61 :
62 2 : for (final event in filteredEvents) {
63 2 : final key = event.keys.first;
64 0 : assert(key != null);
65 1 : switch (event.type) {
66 1 : case DataGraphEventType.addNode:
67 3 : list.add(localAdapter.findOne(key));
68 : break;
69 1 : case DataGraphEventType.updateNode:
70 4 : final idx = list.indexWhere((model) => model?._key == key);
71 3 : list[idx] = localAdapter.findOne(key);
72 : break;
73 1 : case DataGraphEventType.removeNode:
74 4 : list..removeWhere((model) => model?._key == key);
75 : break;
76 : default:
77 : }
78 : }
79 :
80 4 : _notifier.data = _notifier.data.copyWith(model: list, isLoading: false);
81 : });
82 :
83 2 : _notifier.onDispose = _graphNotifier.dispose;
84 : return _notifier;
85 : }
86 :
87 1 : DataStateNotifier<T> watchOne(final dynamic model,
88 : {final bool remote,
89 : final Map<String, dynamic> params,
90 : final Map<String, String> headers,
91 : final AlsoWatch<T> alsoWatch}) {
92 1 : _assertInit();
93 0 : assert(model != null);
94 :
95 2 : final id = model is T ? model.id : model;
96 :
97 : // lazy key access
98 : String _key;
99 1 : String key() => _key ??=
100 5 : graph.getKeyForId(type, id) ?? (model is T ? model._key : null);
101 :
102 : final _alsoWatchFilters = <String>{};
103 : var _relatedKeys = <String>{};
104 :
105 1 : final _notifier = DataStateNotifier<T>(
106 5 : DataState(initializeModel(localAdapter.findOne(key()), save: true)),
107 1 : reload: (notifier) async {
108 : if (id == null) return;
109 : try {
110 1 : final _future = findOne(id,
111 : params: params, headers: headers, remote: remote, init: true);
112 4 : notifier.data = notifier.data.copyWith(isLoading: true);
113 1 : await _future;
114 : } catch (error, stackTrace) {
115 : // we're only interested in notifying errors
116 : // as models will pop up via the graph notifier
117 4 : notifier.data = notifier.data.copyWith(
118 : isLoading: false,
119 : exception: error,
120 : stackTrace: stackTrace,
121 : );
122 : }
123 : },
124 : );
125 :
126 1 : void _initializeRelationshipsToWatch(T model) {
127 : if (alsoWatch != null &&
128 1 : _alsoWatchFilters.isEmpty &&
129 : model != null &&
130 1 : model._isInitialized) {
131 4 : _alsoWatchFilters.addAll(alsoWatch(model).map((rel) {
132 1 : return rel?._name;
133 : }));
134 : }
135 : }
136 :
137 : // kick off
138 :
139 : // try to find relationships to watch
140 3 : _initializeRelationshipsToWatch(_notifier.data.model);
141 :
142 : // trigger local + async loading
143 1 : _notifier.reload();
144 :
145 : // start listening to graph for further changes
146 3 : final _graphNotifier = throttledGraph.forEach((events) {
147 1 : if (!_notifier.mounted) {
148 : return;
149 : }
150 :
151 : // buffers
152 2 : var modelBuffer = _notifier.data.model;
153 : var refresh = false;
154 :
155 2 : for (final event in events) {
156 3 : if (event.keys.containsFirst(key())) {
157 : // add/update
158 2 : if (event.type == DataGraphEventType.addNode ||
159 2 : event.type == DataGraphEventType.updateNode) {
160 3 : final model = localAdapter.findOne(key());
161 : if (model != null) {
162 1 : initializeModel(model, save: true);
163 1 : _initializeRelationshipsToWatch(model);
164 : modelBuffer = model;
165 : }
166 : }
167 :
168 : // remove
169 2 : if (event.type == DataGraphEventType.removeNode &&
170 2 : _notifier.data.model != null) {
171 : modelBuffer = null;
172 : }
173 :
174 : // changes on specific relationships of this model
175 2 : if (_notifier.data.model != null &&
176 2 : event.type.isEdge &&
177 2 : _alsoWatchFilters.contains(event.metadata)) {
178 : // calculate currently related models
179 1 : _relatedKeys = localAdapter
180 3 : .relationshipsFor(_notifier.data.model)
181 1 : .values
182 4 : .map((meta) => (meta['instance'] as Relationship)?.keys)
183 2 : .where((keys) => keys != null)
184 2 : .expand((key) => key)
185 1 : .toSet();
186 :
187 : refresh = true;
188 : }
189 : }
190 :
191 : // updates on all models of specific relationships of this model
192 2 : if (event.type == DataGraphEventType.updateNode &&
193 3 : _relatedKeys.any(event.keys.contains)) {
194 : refresh = true;
195 : }
196 : }
197 :
198 : // NOTE: because of this comparison, use field equality
199 : // rather than key equality (which wouldn't update)
200 3 : if (modelBuffer != _notifier.data.model || refresh) {
201 2 : _notifier.data = _notifier.data
202 2 : .copyWith(model: modelBuffer, isLoading: false, exception: null);
203 : }
204 : });
205 :
206 2 : _notifier.onDispose = _graphNotifier.dispose;
207 : return _notifier;
208 : }
209 : }
210 :
211 : typedef AlsoWatch<T> = List<Relationship> Function(T);
|