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 : DelayedStateNotifier<List<DataGraphEvent>> get throttledGraph =>
12 4 : graph.throttle(() => throttleDuration);
13 :
14 1 : @protected
15 : @visibleForTesting
16 : DataStateNotifier<List<T>> watchAll({
17 : bool? remote,
18 : Map<String, dynamic>? params,
19 : Map<String, String>? headers,
20 : bool? syncLocal,
21 : bool Function(T)? filterLocal,
22 : }) {
23 1 : _assertInit();
24 1 : remote ??= _remote;
25 : syncLocal ??= false;
26 1 : filterLocal ??= (_) => true;
27 :
28 1 : final _notifier = DataStateNotifier<List<T>>(
29 2 : data: DataState(localAdapter
30 1 : .findAll()
31 1 : .where(filterLocal)
32 3 : .map((m) => initializeModel(m, save: true))
33 1 : .filterNulls
34 1 : .toList()),
35 1 : reload: (notifier) async {
36 1 : if (!notifier.mounted) {
37 : return;
38 : }
39 : try {
40 1 : final _future = findAll(
41 : params: params,
42 : headers: headers,
43 : remote: remote,
44 : syncLocal: syncLocal,
45 : filterLocal: filterLocal,
46 : );
47 : if (remote!) {
48 1 : notifier.updateWith(isLoading: true);
49 : }
50 1 : await _future;
51 : // trigger doneLoading to ensure state is updated with isLoading=false
52 4 : graph._notify([internalType], DataGraphEventType.doneLoading);
53 1 : } on DataException catch (e) {
54 : // we're only interested in notifying errors
55 : // as models will pop up via the graph notifier
56 : // (we can catch the exception as we are NOT supplying
57 : // an `onError` to `findAll`)
58 1 : if (notifier.mounted) {
59 1 : notifier.updateWith(isLoading: false, exception: e);
60 : } else {
61 : rethrow;
62 : }
63 : }
64 : },
65 : );
66 :
67 : // kick off
68 1 : _notifier.reload();
69 :
70 3 : final _graphNotifier = throttledGraph.forEach((events) {
71 1 : if (!_notifier.mounted) {
72 : return;
73 : }
74 :
75 : final models =
76 4 : localAdapter.findAll().where(filterLocal!).toImmutableList();
77 : final modelChanged =
78 3 : !const DeepCollectionEquality().equals(models, _notifier.data.model);
79 : // ensure the done signal belongs to this notifier
80 2 : final doneLoading = events.singleWhereOrNull((e) =>
81 2 : e.type == DataGraphEventType.doneLoading &&
82 4 : e.keys.first == internalType) !=
83 : null;
84 : if (modelChanged || doneLoading) {
85 1 : _notifier.updateWith(model: models, isLoading: false, exception: null);
86 : }
87 : });
88 :
89 2 : _notifier.onDispose = _graphNotifier.dispose;
90 : return _notifier;
91 : }
92 :
93 1 : @protected
94 : @visibleForTesting
95 : DataStateNotifier<T?> watchOne(
96 : dynamic model, {
97 : bool? remote,
98 : Map<String, dynamic>? params,
99 : Map<String, String>? headers,
100 : AlsoWatch<T>? alsoWatch,
101 : }) {
102 1 : _assertInit();
103 : if (model == null) {
104 0 : throw AssertionError();
105 : }
106 1 : remote ??= _remote;
107 :
108 1 : final id = _resolveId(model);
109 :
110 : // lazy key access
111 1 : String? key() {
112 3 : return graph.getKeyForId(internalType, id,
113 2 : keyIfAbsent: (model is T ? model._key : null));
114 : }
115 :
116 1 : var _key = key();
117 :
118 : final _alsoWatchFilters = <String>{};
119 :
120 2 : final localModel = _key != null ? localAdapter.findOne(_key) : null;
121 1 : final _notifier = DataStateNotifier<T?>(
122 1 : data: DataState(
123 1 : localModel == null ? null : initializeModel(localModel, save: true)),
124 1 : reload: (notifier) async {
125 1 : if (!notifier.mounted) {
126 : return;
127 : }
128 : try {
129 : if (id != null) {
130 1 : final _future = findOne(
131 : id,
132 : params: params,
133 : headers: headers,
134 : remote: remote,
135 : );
136 : if (remote!) {
137 1 : notifier.updateWith(isLoading: true);
138 : }
139 1 : await _future;
140 : }
141 :
142 1 : _key ??= key();
143 : if (_key != null) {
144 : // trigger doneLoading to ensure state is updated with isLoading=false
145 3 : graph._notify([_key!], DataGraphEventType.doneLoading);
146 : }
147 1 : } on DataException catch (e) {
148 : // we're only interested in notifying errors
149 : // as models will pop up via the graph notifier
150 : // (we can catch the exception as we are NOT supplying
151 : // an `onError` to `findOne`)
152 1 : if (notifier.mounted) {
153 1 : notifier.updateWith(isLoading: false, exception: e);
154 : } else {
155 : rethrow;
156 : }
157 : }
158 : },
159 : );
160 :
161 1 : void _initializeRelationshipsToWatch(T? model) {
162 : if (alsoWatch != null &&
163 1 : _alsoWatchFilters.isEmpty &&
164 : model != null &&
165 1 : model.isInitialized) {
166 4 : _alsoWatchFilters.addAll(alsoWatch(model).map((rel) {
167 1 : return rel._name;
168 : }));
169 : }
170 : }
171 :
172 : // kick off
173 :
174 : // try to find relationships to watch
175 3 : _initializeRelationshipsToWatch(_notifier.data.model);
176 :
177 : // trigger local + async loading
178 1 : _notifier.reload();
179 :
180 : // start listening to graph for further changes
181 3 : final _graphNotifier = throttledGraph.forEach((events) {
182 1 : if (!_notifier.mounted) {
183 : return;
184 : }
185 :
186 : // buffers
187 2 : var modelBuffer = _notifier.data.model;
188 : var refresh = false;
189 :
190 2 : for (final event in events) {
191 1 : _key ??= key();
192 2 : if (_key != null && event.keys.containsFirst(_key!)) {
193 : // add/update
194 2 : if (event.type == DataGraphEventType.addNode ||
195 2 : event.type == DataGraphEventType.updateNode) {
196 2 : final model = localAdapter.findOne(_key!);
197 : if (model != null) {
198 1 : initializeModel(model, save: true);
199 1 : _initializeRelationshipsToWatch(model);
200 : modelBuffer = model;
201 : }
202 : }
203 :
204 : // remove
205 2 : if (event.type == DataGraphEventType.removeNode &&
206 2 : _notifier.data.model != null) {
207 : modelBuffer = null;
208 : }
209 :
210 : // changes on specific relationships of this model
211 2 : if (_notifier.data.model != null &&
212 2 : event.type.isEdge &&
213 2 : _alsoWatchFilters.contains(event.metadata)) {
214 : // calculate currently related models
215 : refresh = true;
216 : }
217 :
218 : if (modelBuffer != null &&
219 : // ensure the done signal belongs to this type
220 3 : event.keys.first == _key &&
221 2 : event.type == DataGraphEventType.doneLoading) {
222 : refresh = true;
223 : }
224 : }
225 :
226 : // updates on all models of specific relationships of this model
227 2 : if (event.type == DataGraphEventType.updateNode &&
228 6 : _relatedKeys(_notifier.data.model!).any(event.keys.contains)) {
229 : refresh = true;
230 : }
231 : }
232 :
233 : // NOTE: because of this comparison, use field equality
234 : // rather than key equality (which wouldn't update)
235 3 : if (modelBuffer != _notifier.data.model || refresh) {
236 1 : _notifier.updateWith(
237 : model: modelBuffer, isLoading: false, exception: null);
238 : }
239 : });
240 :
241 2 : _notifier.onDispose = _graphNotifier.dispose;
242 : return _notifier;
243 : }
244 :
245 1 : Set<String> _relatedKeys(T model) {
246 1 : return localAdapter
247 1 : .relationshipsFor(model)
248 1 : .values
249 4 : .map((meta) => (meta['instance'] as Relationship?)?.keys)
250 1 : .filterNulls
251 2 : .expand((key) => key)
252 1 : .toSet();
253 : }
254 : }
255 :
256 : typedef AlsoWatch<T> = List<Relationship> Function(T);
|