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
}
@@ -289,10 +317,12 @@ mixin _MessageSequence {
289
317
/// Update [items] to include markers at start and end as appropriate.
290
318
void _updateEndMarkers () {
291
319
assert (! (haveOldest && fetchingOlder));
292
- final startMarker = switch ((fetchingOlder, haveOldest)) {
293
- (true , _) => const MessageListLoadingItem (MessageListDirection .older),
294
- (_, true ) => const MessageListHistoryStartItem (),
295
- (_, _) => null ,
320
+ assert (! (fetchingOlder && fetchOlderCoolingDown));
321
+ final startMarker = switch ((fetchingOlder, haveOldest, fetchOlderCoolingDown)) {
322
+ (true , _, _) => const MessageListLoadingItem (MessageListDirection .older),
323
+ (_, true , _) => const MessageListHistoryStartItem (),
324
+ (_, _, true ) => const MessageListLoadingItem (MessageListDirection .older),
325
+ (_, _, _) => null ,
296
326
};
297
327
final hasStartMarker = switch (items.firstOrNull) {
298
328
MessageListLoadingItem () => true ,
@@ -469,7 +499,7 @@ class MessageListView with ChangeNotifier, _MessageSequence {
469
499
Future <void > fetchInitial () async {
470
500
// TODO(#80): fetch from anchor firstUnread, instead of newest
471
501
// TODO(#82): fetch from a given message ID as anchor
472
- assert (! fetched && ! haveOldest && ! fetchingOlder);
502
+ assert (! fetched && ! haveOldest && ! fetchingOlder && ! fetchOlderCoolingDown );
473
503
assert (messages.isEmpty && contents.isEmpty);
474
504
// TODO schedule all this in another isolate
475
505
final generation = this .generation;
@@ -497,20 +527,30 @@ class MessageListView with ChangeNotifier, _MessageSequence {
497
527
Future <void > fetchOlder () async {
498
528
if (haveOldest) return ;
499
529
if (fetchingOlder) return ;
530
+ if (fetchOlderCoolingDown) return ;
500
531
assert (fetched);
501
532
assert (messages.isNotEmpty);
502
533
_fetchingOlder = true ;
503
534
_updateEndMarkers ();
504
535
notifyListeners ();
505
536
final generation = this .generation;
506
537
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
- );
538
+ final GetMessagesResult result;
539
+ try {
540
+ result = await getMessages (store.connection,
541
+ narrow: narrow.apiEncode (),
542
+ anchor: NumericAnchor (messages[0 ].id),
543
+ includeAnchor: false ,
544
+ numBefore: kMessageListFetchBatchSize,
545
+ numAfter: 0 ,
546
+ );
547
+ } catch (e) {
548
+ assert (! fetchOlderCoolingDown);
549
+ _fetchOlderCoolingDown = true ;
550
+ unawaited ((_fetchOlderCooldownBackoffMachine ?? = BackoffMachine ())
551
+ .wait ().then ((_) => _fetchOlderCoolingDown = false ));
552
+ rethrow ;
553
+ }
514
554
if (this .generation > generation) return ;
515
555
516
556
if (result.messages.isNotEmpty
@@ -528,6 +568,7 @@ class MessageListView with ChangeNotifier, _MessageSequence {
528
568
529
569
_insertAllMessages (0 , fetchedMessages);
530
570
_haveOldest = result.foundOldest;
571
+ _fetchOlderCooldownBackoffMachine = null ;
531
572
} finally {
532
573
if (this .generation == generation) {
533
574
_fetchingOlder = false ;
0 commit comments