1
+ import 'dart:async' ;
2
+
1
3
import 'package:collection/collection.dart' ;
2
4
import 'package:flutter/foundation.dart' ;
3
5
6
+ import '../api/backoff.dart' ;
4
7
import '../api/model/events.dart' ;
5
8
import '../api/model/model.dart' ;
6
9
import '../api/route/messages.dart' ;
@@ -89,9 +92,32 @@ mixin _MessageSequence {
89
92
bool _haveOldest = false ;
90
93
91
94
/// Whether we are currently fetching the next batch of older messages.
95
+ ///
96
+ /// When this is true, [fetchOlder] is a no-op.
97
+ /// That method is called frequently by Flutter's scrolling logic,
98
+ /// and this field helps us avoid spamming the same request just to get
99
+ /// the same response each time.
100
+ ///
101
+ /// See also [fetchOlderCoolingDown] .
92
102
bool get fetchingOlder => _fetchingOlder;
93
103
bool _fetchingOlder = false ;
94
104
105
+ /// Whether [fetchOlder] had a request error recently.
106
+ ///
107
+ /// When this is true, [fetchOlder] is a no-op.
108
+ /// That method is called frequently by Flutter's scrolling logic,
109
+ /// and this field mitigates spamming the same request and getting
110
+ /// the same error each time.
111
+ ///
112
+ /// "Recently" is decided by a [BackoffMachine] that resets
113
+ /// when a [fetchOlder] request succeeds.
114
+ ///
115
+ /// See also [fetchingOlder] .
116
+ bool get fetchOlderCoolingDown => _fetchOlderCoolingDown;
117
+ bool _fetchOlderCoolingDown = false ;
118
+
119
+ BackoffMachine ? _fetchOlderCooldownBackoffMachine;
120
+
95
121
/// The parsed message contents, as a list parallel to [messages] .
96
122
///
97
123
/// The i'th element is the result of parsing the i'th element of [messages] .
@@ -107,7 +133,7 @@ mixin _MessageSequence {
107
133
/// before, between, or after the messages.
108
134
///
109
135
/// This information is completely derived from [messages] and
110
- /// the flags [haveOldest] and [fetchingOlder ] .
136
+ /// the flags [haveOldest] , [fetchingOlder] and [fetchOlderCoolingDown ] .
111
137
/// It exists as an optimization, to memoize that computation.
112
138
final QueueList <MessageListItem > items = QueueList ();
113
139
@@ -241,6 +267,8 @@ mixin _MessageSequence {
241
267
_fetched = false ;
242
268
_haveOldest = false ;
243
269
_fetchingOlder = false ;
270
+ _fetchOlderCoolingDown = false ;
271
+ _fetchOlderCooldownBackoffMachine = null ;
244
272
contents.clear ();
245
273
items.clear ();
246
274
}
@@ -288,11 +316,14 @@ mixin _MessageSequence {
288
316
289
317
/// Update [items] to include markers at start and end as appropriate.
290
318
void _updateEndMarkers () {
319
+ assert (fetched);
291
320
assert (! (haveOldest && fetchingOlder));
292
- final startMarker = switch ((fetchingOlder, haveOldest)) {
293
- (true , _) => const MessageListLoadingItem (MessageListDirection .older),
294
- (_, true ) => const MessageListHistoryStartItem (),
295
- (_, _) => null ,
321
+ assert (! (fetchingOlder && fetchOlderCoolingDown));
322
+ final startMarker = switch ((fetchingOlder, haveOldest, fetchOlderCoolingDown)) {
323
+ (true , _, _) => const MessageListLoadingItem (MessageListDirection .older),
324
+ (_, true , _) => const MessageListHistoryStartItem (),
325
+ (_, _, true ) => const MessageListLoadingItem (MessageListDirection .older),
326
+ (_, _, _) => null ,
296
327
};
297
328
final hasStartMarker = switch (items.firstOrNull) {
298
329
MessageListLoadingItem () => true ,
@@ -469,7 +500,7 @@ class MessageListView with ChangeNotifier, _MessageSequence {
469
500
Future <void > fetchInitial () async {
470
501
// TODO(#80): fetch from anchor firstUnread, instead of newest
471
502
// TODO(#82): fetch from a given message ID as anchor
472
- assert (! fetched && ! haveOldest && ! fetchingOlder);
503
+ assert (! fetched && ! haveOldest && ! fetchingOlder && ! fetchOlderCoolingDown );
473
504
assert (messages.isEmpty && contents.isEmpty);
474
505
// TODO schedule all this in another isolate
475
506
final generation = this .generation;
@@ -497,20 +528,28 @@ class MessageListView with ChangeNotifier, _MessageSequence {
497
528
Future <void > fetchOlder () async {
498
529
if (haveOldest) return ;
499
530
if (fetchingOlder) return ;
531
+ if (fetchOlderCoolingDown) return ;
500
532
assert (fetched);
501
533
assert (messages.isNotEmpty);
502
534
_fetchingOlder = true ;
503
535
_updateEndMarkers ();
504
536
notifyListeners ();
505
537
final generation = this .generation;
538
+ bool hasFetchError = false ;
506
539
try {
507
- final result = await getMessages (store.connection,
508
- narrow: narrow.apiEncode (),
509
- anchor: NumericAnchor (messages[0 ].id),
510
- includeAnchor: false ,
511
- numBefore: kMessageListFetchBatchSize,
512
- numAfter: 0 ,
513
- );
540
+ final GetMessagesResult result;
541
+ try {
542
+ result = await getMessages (store.connection,
543
+ narrow: narrow.apiEncode (),
544
+ anchor: NumericAnchor (messages[0 ].id),
545
+ includeAnchor: false ,
546
+ numBefore: kMessageListFetchBatchSize,
547
+ numAfter: 0 ,
548
+ );
549
+ } catch (e) {
550
+ hasFetchError = true ;
551
+ rethrow ;
552
+ }
514
553
if (this .generation > generation) return ;
515
554
516
555
if (result.messages.isNotEmpty
@@ -528,12 +567,29 @@ class MessageListView with ChangeNotifier, _MessageSequence {
528
567
529
568
_insertAllMessages (0 , fetchedMessages);
530
569
_haveOldest = result.foundOldest;
570
+ _fetchOlderCooldownBackoffMachine = null ;
531
571
} finally {
532
- if (this .generation == generation) {
533
- _fetchingOlder = false ;
534
- _updateEndMarkers ();
535
- notifyListeners ();
572
+ if (this .generation != generation) {
573
+ // We need the finally block always clean up regardless of errors
574
+ // occured in the try block, and returning early here is necessary
575
+ // if such cleanup must be skipped, as the fetch is considered stale.
576
+ // ignore: control_flow_in_finally
577
+ return ;
578
+ }
579
+ _fetchingOlder = false ;
580
+ if (hasFetchError) {
581
+ assert (! fetchOlderCoolingDown);
582
+ _fetchOlderCoolingDown = true ;
583
+ unawaited ((_fetchOlderCooldownBackoffMachine ?? = BackoffMachine ())
584
+ .wait ().then ((_) {
585
+ if (this .generation != generation) return ;
586
+ _fetchOlderCoolingDown = false ;
587
+ _updateEndMarkers ();
588
+ notifyListeners ();
589
+ }));
536
590
}
591
+ _updateEndMarkers ();
592
+ notifyListeners ();
537
593
}
538
594
}
539
595
0 commit comments