@@ -52,10 +52,13 @@ class UpdateNotification {
5252 static Stream <UpdateNotification > throttleStream (
5353 Stream <UpdateNotification > input, Duration timeout,
5454 {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+ );
5962 }
6063
6164 /// Filter an update stream by specific tables.
@@ -67,62 +70,112 @@ class UpdateNotification {
6770 }
6871}
6972
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] .
7275///
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+ }
87113 }
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+ }
90127 }
91- }, onDone: () {
92- if (! nextPing.isCompleted) {
93- nextPing.complete ();
128+
129+ void setTimeout () {
130+ activeTimeoutWindow = Timer (timeout, () {
131+ activeTimeoutWindow = null ;
132+ maybeEmit ();
133+ });
94134 }
95135
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+ }
98143
99- try {
100- if (addOne != null ) {
101- yield addOne ;
144+ void onError ( Object error, StackTrace trace) {
145+ addPendingEvents ();
146+ listener. addErrorSync (error, trace) ;
102147 }
103- if (throttleFirst) {
104- await Future .delayed (timeout);
148+
149+ void onDone () {
150+ addPendingEvents ();
151+ listener.closeSync ();
105152 }
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);
120176 }
121- } finally {
122- if (lastData case final data? ) {
123- yield data;
177+ if (throttleFirst) {
178+ setTimeout ();
124179 }
125-
126- await listener.cancel ();
127- }
180+ });
128181}
0 commit comments