@@ -52,10 +52,13 @@ class UpdateNotification {
52
52
static Stream <UpdateNotification > throttleStream (
53
53
Stream <UpdateNotification > input, Duration timeout,
54
54
{UpdateNotification ? addOne}) {
55
- return _throttleStream (input, timeout, addOne: addOne, throttleFirst: true ,
56
- add: (a, b) {
57
- return a.union (b);
58
- });
55
+ return _throttleStream (
56
+ input: input,
57
+ timeout: timeout,
58
+ throttleFirst: true ,
59
+ add: (a, b) => a.union (b),
60
+ addOne: addOne,
61
+ );
59
62
}
60
63
61
64
/// Filter an update stream by specific tables.
@@ -67,62 +70,112 @@ class UpdateNotification {
67
70
}
68
71
}
69
72
70
- /// Given a broadcast stream, return a singular throttled stream that is throttled.
71
- /// This immediately starts listening .
73
+ /// Throttles an [input] stream to not emit events more often than with a
74
+ /// frequency of 1/ [timeout] .
72
75
///
73
- /// Behaviour:
74
- /// If there was no event in "timeout", and one comes in, it is pushed immediately.
75
- /// Otherwise, we wait until the timeout is over.
76
- Stream <T > _throttleStream <T extends Object >(Stream <T > input, Duration timeout,
77
- {bool throttleFirst = false , T Function (T , T )? add, T ? addOne}) async * {
78
- var nextPing = Completer <void >();
79
- var done = false ;
80
- T ? lastData;
81
-
82
- var listener = input.listen ((data) {
83
- if (lastData != null && add != null ) {
84
- lastData = add (lastData! , data);
85
- } else {
86
- lastData = data;
76
+ /// When an event is received and no timeout window is active, it is forwarded
77
+ /// downstream and a timeout window is started. For events received within a
78
+ /// timeout window, [add] is called to fold events. Then when the window
79
+ /// expires, pending events are emitted.
80
+ /// The subscription to the [input] stream is never paused.
81
+ ///
82
+ /// When the returned stream is paused, an active timeout window is reset and
83
+ /// restarts after the stream is resumed.
84
+ ///
85
+ /// If [addOne] is not null, that event will always be added when the stream is
86
+ /// subscribed to.
87
+ /// When [throttleFirst] is true, a timeout window begins immediately after
88
+ /// listening (so that the first event, apart from [addOne] , is emitted no
89
+ /// earlier than after [timeout] ).
90
+ Stream <T > _throttleStream <T extends Object >({
91
+ required Stream <T > input,
92
+ required Duration timeout,
93
+ required bool throttleFirst,
94
+ required T Function (T , T ) add,
95
+ required T ? addOne,
96
+ }) {
97
+ return Stream .multi ((listener) {
98
+ T ? pendingData;
99
+ Timer ? activeTimeoutWindow;
100
+
101
+ /// Add pending data, bypassing the active timeout window.
102
+ ///
103
+ /// This is used to forward error and done events immediately.
104
+ bool addPendingEvents () {
105
+ if (pendingData case final data? ) {
106
+ pendingData = null ;
107
+ listener.addSync (data);
108
+ activeTimeoutWindow? .cancel ();
109
+ return true ;
110
+ } else {
111
+ return false ;
112
+ }
87
113
}
88
- if (! nextPing.isCompleted) {
89
- nextPing.complete ();
114
+
115
+ /// Emits [pendingData] if no timeout window is active, and then starts a
116
+ /// timeout window if necessary.
117
+ void maybeEmit () {
118
+ if (activeTimeoutWindow == null && ! listener.isPaused) {
119
+ final didAdd = addPendingEvents ();
120
+ if (didAdd) {
121
+ activeTimeoutWindow = Timer (timeout, () {
122
+ activeTimeoutWindow = null ;
123
+ maybeEmit ();
124
+ });
125
+ }
126
+ }
90
127
}
91
- }, onDone: () {
92
- if (! nextPing.isCompleted) {
93
- nextPing.complete ();
128
+
129
+ void setTimeout () {
130
+ activeTimeoutWindow = Timer (timeout, () {
131
+ activeTimeoutWindow = null ;
132
+ maybeEmit ();
133
+ });
94
134
}
95
135
96
- done = true ;
97
- });
136
+ void onData (T data) {
137
+ pendingData = switch (pendingData) {
138
+ null => data,
139
+ final pending => add (pending, data),
140
+ };
141
+ maybeEmit ();
142
+ }
98
143
99
- try {
100
- if (addOne != null ) {
101
- yield addOne ;
144
+ void onError ( Object error, StackTrace trace) {
145
+ addPendingEvents ();
146
+ listener. addErrorSync (error, trace) ;
102
147
}
103
- if (throttleFirst) {
104
- await Future .delayed (timeout);
148
+
149
+ void onDone () {
150
+ addPendingEvents ();
151
+ listener.closeSync ();
105
152
}
106
- while (! done) {
107
- // If a value is available now, we'll use it immediately.
108
- // If not, this waits for it.
109
- await nextPing.future;
110
- if (done) break ;
111
-
112
- // Capture any new values coming in while we wait.
113
- nextPing = Completer <void >();
114
- T data = lastData as T ;
115
- // Clear before we yield, so that we capture new changes while yielding
116
- lastData = null ;
117
- yield data;
118
- // Wait a minimum of this duration between tasks
119
- await Future .delayed (timeout);
153
+
154
+ final subscription = input.listen (onData, onError: onError, onDone: onDone);
155
+ var needsTimeoutWindowAfterResume = false ;
156
+
157
+ listener.onPause = () {
158
+ needsTimeoutWindowAfterResume = activeTimeoutWindow != null ;
159
+ activeTimeoutWindow? .cancel ();
160
+ };
161
+ listener.onResume = () {
162
+ if (needsTimeoutWindowAfterResume) {
163
+ setTimeout ();
164
+ } else {
165
+ maybeEmit ();
166
+ }
167
+ };
168
+ listener.onCancel = () async {
169
+ activeTimeoutWindow? .cancel ();
170
+ return subscription.cancel ();
171
+ };
172
+
173
+ if (addOne != null ) {
174
+ // This must not be sync, we're doing this directly in onListen
175
+ listener.add (addOne);
120
176
}
121
- } finally {
122
- if (lastData case final data? ) {
123
- yield data;
177
+ if (throttleFirst) {
178
+ setTimeout ();
124
179
}
125
-
126
- await listener.cancel ();
127
- }
180
+ });
128
181
}
0 commit comments