Line data Source code
1 : part of flutter_data;
2 :
3 : mixin _RemoteAdapterWatch<T extends DataModel<T>> on _RemoteAdapter<T> {
4 2 : @protected
5 : DataStateNotifier<List<T>?> watchAllNotifier({
6 : bool? remote,
7 : Map<String, dynamic>? params,
8 : Map<String, String>? headers,
9 : bool? syncLocal,
10 : String? finder,
11 : DataRequestLabel? label,
12 : }) {
13 0 : remote ??= _remote;
14 : syncLocal ??= false;
15 :
16 6 : final maybeFinder = _internalHolder?.finders[finder]?.call(this);
17 4 : final finderFn = maybeFinder is DataFinderAll<T> ? maybeFinder : findAll;
18 :
19 : // we can't use `findAll`'s default internal label
20 : // because we need a label to handle events
21 4 : label ??= DataRequestLabel('watchAll', type: internalType);
22 :
23 2 : log(label, 'initializing');
24 :
25 : // closure to get latest models
26 2 : List<T>? _getUpdatedModels() {
27 4 : return localAdapter.findAll();
28 : }
29 :
30 2 : final notifier = DataStateNotifier<List<T>?>(
31 4 : data: DataState(_getUpdatedModels(), isLoading: remote!),
32 : );
33 :
34 4 : notifier._reloadFn = () async {
35 2 : if (!notifier.mounted) {
36 : return;
37 : }
38 :
39 : if (remote!) {
40 2 : notifier.updateWith(isLoading: true);
41 : }
42 :
43 4 : await finderFn(
44 : remote: remote,
45 : params: params,
46 : headers: headers,
47 : syncLocal: syncLocal,
48 : label: label,
49 2 : onError: (e, label, _) async {
50 : try {
51 3 : await onError<List<T>>(e, label);
52 1 : } on DataException catch (err) {
53 : e = err;
54 : } catch (_) {
55 : rethrow;
56 : }
57 2 : if (notifier.mounted) {
58 2 : notifier.updateWith(isLoading: false, exception: e);
59 : }
60 : return null;
61 : },
62 : );
63 : if (remote) {
64 : // trigger doneLoading to ensure state is updated with isLoading=false
65 8 : graph._notify([label.toString()], type: DataGraphEventType.doneLoading);
66 : }
67 : };
68 :
69 : // kick off
70 2 : notifier.reload();
71 :
72 : late DelayedStateNotifier<List<DataGraphEvent>> throttledGraph;
73 :
74 6 : final throttleDuration = read(graphNotifierThrottleDurationProvider);
75 : if (throttleDuration != null) {
76 0 : throttledGraph = graph.throttle(() => throttleDuration);
77 : } else {
78 : // if no throttle is required, use map to
79 : // convert a single event in a list of events
80 8 : throttledGraph = graph.map((_) => [_]);
81 : }
82 :
83 2 : final states = <DataState<List<T>?>>[];
84 :
85 4 : final dispose = throttledGraph.addListener((events) {
86 2 : if (!notifier.mounted) {
87 : return;
88 : }
89 :
90 4 : for (final event in events) {
91 : // handle done loading
92 4 : if (notifier.data.isLoading &&
93 8 : event.keys.last == label.toString() &&
94 4 : event.type == DataGraphEventType.doneLoading) {
95 2 : final models = _getUpdatedModels();
96 4 : states.add(DataState(models, isLoading: false, exception: null));
97 : }
98 :
99 6 : if (notifier.data.isLoading == false &&
100 4 : event.type.isNode &&
101 8 : event.keys.first.startsWith(internalType)) {
102 2 : final models = _getUpdatedModels();
103 2 : log(label!, 'updated models', logLevel: 2);
104 4 : states.add(DataState(
105 : models,
106 4 : isLoading: notifier.data.isLoading,
107 4 : exception: notifier.data.exception,
108 4 : stackTrace: notifier.data.stackTrace,
109 : ));
110 : }
111 : }
112 :
113 2 : _updateFromStates(states, notifier);
114 : });
115 :
116 4 : notifier.onDispose = () {
117 2 : log(label!, 'disposing');
118 2 : dispose();
119 : };
120 : return notifier;
121 : }
122 :
123 2 : @protected
124 : DataStateNotifier<T?> watchOneNotifier(
125 : String key, {
126 : bool? remote,
127 : Map<String, dynamic>? params,
128 : Map<String, String>? headers,
129 : AlsoWatch<T>? alsoWatch,
130 : String? finder,
131 : DataRequestLabel? label,
132 : }) {
133 4 : final id = graph.getIdForKey(key);
134 :
135 0 : remote ??= _remote;
136 7 : final maybeFinder = _internalHolder?.finders[finder]?.call(this);
137 4 : final finderFn = maybeFinder is DataFinderOne<T> ? maybeFinder : findOne;
138 :
139 : // we can't use `findOne`'s default internal label
140 : // because we need a label to handle events
141 : label ??=
142 6 : DataRequestLabel('watchOne', id: key.detypify(), type: internalType);
143 :
144 : var alsoWatchPairs = <List<String>>{};
145 :
146 : // closure to get latest model and watchable relationship pairs
147 2 : T? _getUpdatedModel() {
148 4 : final model = localAdapter.findOne(key);
149 : if (model != null) {
150 : // get all metas provided via `alsoWatch`
151 : final metas = alsoWatch
152 2 : ?.call(RelationshipGraphNode<T>())
153 1 : .whereType<RelationshipMeta>();
154 :
155 : // recursively get applicable watch key pairs for each meta -
156 : // from top to bottom (e.g. `p`, `p.familia`, `p.familia.cottage`)
157 : alsoWatchPairs = {
158 1 : ...?metas
159 4 : ?.map((meta) => _getPairsForMeta(meta._top, model))
160 1 : .filterNulls
161 2 : .expand((_) => _)
162 : };
163 : } else {
164 : // if there is no model nothing should be watched, reset pairs
165 : alsoWatchPairs = {};
166 : }
167 : return model;
168 : }
169 :
170 2 : final notifier = DataStateNotifier<T?>(
171 4 : data: DataState(_getUpdatedModel(), isLoading: remote!),
172 : );
173 :
174 : final alsoWatchNames = alsoWatch
175 2 : ?.call(RelationshipGraphNode<T>())
176 1 : .whereType<RelationshipMeta>()
177 3 : .map((m) => m.name) ??
178 : {};
179 2 : log(label,
180 6 : 'initializing${alsoWatchNames.isNotEmpty ? ' (and also watching: ${alsoWatchNames.join(', ')})' : ''}');
181 :
182 4 : notifier._reloadFn = () async {
183 2 : if (!notifier.mounted || id == null) return;
184 :
185 : if (remote!) {
186 1 : notifier.updateWith(isLoading: true);
187 : }
188 :
189 4 : final model = await finderFn(
190 : id,
191 : remote: remote,
192 : params: params,
193 : headers: headers,
194 : label: label,
195 1 : onError: (e, label, _) async {
196 : try {
197 1 : await onError<T>(e, label);
198 1 : } on DataException catch (err) {
199 : e = err;
200 : } catch (_) {
201 : rethrow;
202 : }
203 1 : if (notifier.mounted) {
204 1 : notifier.updateWith(isLoading: false, exception: e);
205 : }
206 : return null;
207 : },
208 : );
209 : // trigger doneLoading to ensure state is updated with isLoading=false
210 2 : final modelKey = model?._key;
211 : if (remote && modelKey != null) {
212 4 : graph._notify([modelKey, label.toString()],
213 : type: DataGraphEventType.doneLoading);
214 : }
215 : };
216 :
217 : // trigger local + async loading
218 2 : notifier.reload();
219 :
220 : // local buffer useful to reduce amount of notifier updates
221 4 : var bufferModel = notifier.data.model;
222 :
223 : late DelayedStateNotifier<List<DataGraphEvent>> throttledGraph;
224 :
225 6 : final throttleDuration = read(graphNotifierThrottleDurationProvider);
226 : if (throttleDuration != null) {
227 0 : throttledGraph = graph.throttle(() => throttleDuration);
228 : } else {
229 : // if no throttle is required, use map to
230 : // convert a single event in a list of events
231 8 : throttledGraph = graph.map((_) => [_]);
232 : }
233 :
234 2 : final states = <DataState<T?>>[];
235 :
236 : // start listening to graph for further changes
237 4 : final dispose = throttledGraph.addListener((events) {
238 2 : if (!notifier.mounted) return;
239 :
240 : // get the latest updated model with watchable relationships
241 : // (_alsoWatchPairs) in order to determine whether there is
242 : // something that will cause an event (with the introduction
243 : // of `andEach` even seemingly unrelated models could trigger)
244 2 : bufferModel = _getUpdatedModel();
245 :
246 2 : key = bufferModel?._key ?? key;
247 :
248 4 : for (final event in events) {
249 4 : if (event.keys.contains(key)) {
250 : // handle done loading
251 4 : if (notifier.data.isLoading &&
252 4 : event.keys.last == label.toString() &&
253 2 : event.type == DataGraphEventType.doneLoading) {
254 : states
255 2 : .add(DataState(bufferModel, isLoading: false, exception: null));
256 : }
257 :
258 : // add/update
259 4 : if (event.type == DataGraphEventType.addNode ||
260 4 : event.type == DataGraphEventType.updateNode) {
261 6 : if (notifier.data.isLoading == false) {
262 6 : log(label!, 'added/updated node ${event.keys}', logLevel: 2);
263 4 : states.add(DataState(
264 : bufferModel,
265 4 : isLoading: notifier.data.isLoading,
266 4 : exception: notifier.data.exception,
267 4 : stackTrace: notifier.data.stackTrace,
268 : ));
269 : }
270 : }
271 :
272 : // temporarily restore removed pair so that watchedRelationshipUpdate
273 : // has a chance to apply the update
274 4 : if (event.type == DataGraphEventType.removeEdge &&
275 3 : !event.keys.first.startsWith('id:')) {
276 2 : alsoWatchPairs.add(event.keys);
277 : }
278 : }
279 :
280 : // handle deletion
281 4 : if (event.type == DataGraphEventType.removeNode &&
282 : bufferModel == null) {
283 3 : log(label!, 'removed node ${event.keys}', logLevel: 2);
284 2 : states.add(DataState(
285 : null,
286 2 : isLoading: notifier.data.isLoading,
287 2 : exception: notifier.data.exception,
288 2 : stackTrace: notifier.data.stackTrace,
289 : ));
290 : }
291 :
292 : // updates on watched relationships condition
293 4 : final watchedRelationshipUpdate = event.type.isEdge &&
294 : alsoWatchPairs
295 2 : .where((pair) =>
296 6 : pair.sorted().toString() == event.keys.sorted().toString())
297 1 : .isNotEmpty;
298 :
299 : // updates on watched models (of relationships) condition
300 4 : final watchedModelUpdate = event.type.isNode &&
301 : alsoWatchPairs
302 6 : .where((pair) => pair.contains(event.keys.first))
303 2 : .isNotEmpty;
304 :
305 : // if model is loaded and any condition passes, notify update
306 6 : if (notifier.data.isLoading == false &&
307 : (watchedRelationshipUpdate || watchedModelUpdate)) {
308 3 : log(label!, 'relationship update ${event.keys}', logLevel: 2);
309 2 : states.add(DataState(
310 : bufferModel,
311 2 : isLoading: notifier.data.isLoading,
312 2 : exception: notifier.data.exception,
313 2 : stackTrace: notifier.data.stackTrace,
314 : ));
315 : }
316 : }
317 :
318 2 : _updateFromStates(states, notifier);
319 : });
320 :
321 4 : notifier.onDispose = () {
322 2 : log(label!, 'disposing');
323 2 : dispose();
324 : };
325 : return notifier;
326 : }
327 :
328 3 : void _updateFromStates<S>(
329 : List<DataState<S>> states, DataStateNotifier<S> notifier) {
330 6 : final mergedState = states.fold<DataState<S>?>(null, (acc, state) {
331 0 : return acc == null ? state : acc.merge(state);
332 : });
333 3 : states.clear();
334 :
335 : if (mergedState != null) {
336 3 : notifier.updateWith(
337 3 : model: mergedState.model,
338 3 : isLoading: mergedState.isLoading,
339 3 : exception: mergedState.exception,
340 3 : stackTrace: mergedState.stackTrace,
341 : );
342 : }
343 : }
344 :
345 1 : Iterable<List<String>> _getPairsForMeta(
346 : RelationshipMeta? meta, DataModel model) {
347 : // get instance of this relationship
348 2 : final relationship = meta?.instance(model);
349 : if (relationship == null) return {};
350 :
351 : return {
352 : // include key pairs of (owner, key)
353 3 : for (final key in relationship._keys) [relationship._ownerKey!, key],
354 : // recursively include key pairs for other requested relationships
355 1 : for (final childModel in relationship._iterable)
356 2 : _getPairsForMeta(meta!.child, childModel as DataModel)
357 2 : .expand((_) => _)
358 1 : .toList()
359 : };
360 : }
361 : }
362 :
363 6 : final graphNotifierThrottleDurationProvider =
364 6 : Provider<Duration?>((ref) => null);
|