@@ -52,10 +52,13 @@ class UpdateNotification {
5252static  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