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