Skip to content

Commit 473db05

Browse files
committed
Proposal: returning a second function from batchedThrottle allowing to await the next dispatch
Should debounce and bufferedThrottle follow the same API?
1 parent 6b35d58 commit 473db05

File tree

2 files changed

+90
-40
lines changed

2 files changed

+90
-40
lines changed

src/Fabulous.Tests/CmdTests.fs

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ type ``Cmd tests``() =
162162
messageCount <- messageCount + 1
163163
dispatched <- msg :: dispatched
164164

165-
let batchedThrottleCmd = Cmd.batchedThrottle 100 NewValues
165+
let batchedThrottleCmd, _ = Cmd.batchedThrottle 100 NewValues
166166

167167
batchedThrottleCmd 1 |> CmdTestsHelper.execute dispatch
168168
batchedThrottleCmd 2 |> CmdTestsHelper.execute dispatch
@@ -186,7 +186,7 @@ type ``Cmd tests``() =
186186
messageCount <- messageCount + 1
187187
dispatched <- msg :: dispatched
188188

189-
let batchedThrottleCmd = Cmd.batchedThrottle 100 NewValues
189+
let batchedThrottleCmd, _ = Cmd.batchedThrottle 100 NewValues
190190

191191
batchedThrottleCmd 1 |> CmdTestsHelper.execute dispatch
192192
batchedThrottleCmd 2 |> CmdTestsHelper.execute dispatch
@@ -212,3 +212,29 @@ type ``Cmd tests``() =
212212
Assert.AreEqual(4, messageCount)
213213
Assert.AreEqual([ NewValues[4]; NewValues[3]; NewValues[2]; NewValues[1] ], dispatched)
214214
}
215+
216+
[<Test>]
217+
member _.``Cmd.batchedThrottle factory can be awaited for completion``() =
218+
async {
219+
let mutable messageCount = 0
220+
let mutable dispatched = [] // records dispatched messages latest first
221+
222+
let dispatch msg =
223+
messageCount <- messageCount + 1
224+
dispatched <- msg :: dispatched
225+
226+
let createCmd, awaitNextDispatch = Cmd.batchedThrottle 100 NewValues
227+
228+
createCmd 1 |> CmdTestsHelper.execute dispatch
229+
createCmd 2 |> CmdTestsHelper.execute dispatch
230+
231+
// Only the first value should have been dispatched immediately
232+
Assert.AreEqual(1, messageCount)
233+
Assert.AreEqual([ NewValues[1] ], dispatched)
234+
235+
do! awaitNextDispatch None // only waits until next dispatch
236+
237+
// All values should have been dispatched after waiting
238+
Assert.AreEqual(2, messageCount)
239+
Assert.AreEqual([ NewValues[2]; NewValues[1] ], dispatched)
240+
}

src/Fabulous/Cmd.fs

Lines changed: 62 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -325,17 +325,23 @@ module Cmd =
325325
/// <param name="interval">The minimum time interval between two consecutive Command executions in milliseconds.</param>
326326
/// <param name="fn">A function that maps a list of factory input values to a message for dispatch.</param>
327327
/// <returns>
328-
/// A Command factory function that maps a list of input values to a Command which dispatches a message (mapped from the pending values),
328+
/// Two methods - the first being a Command factory function that maps a list of input values to a Command
329+
/// which dispatches a message (mapped from the pending values),
329330
/// either immediately or after a delay respecting the interval, while remembering and dispatching all remembered values
330331
/// when the interval has elapsed, ensuring no values are lost.
332+
/// The second can be used for awaiting the next dispatch from the outside while adding some buffer time.
331333
/// </returns>
332-
let batchedThrottle (interval: int) (mapValuesToMsg: 'value list -> 'msg) : 'value -> Cmd<'msg> =
334+
let batchedThrottle (interval: int) (mapValuesToMsg: 'value list -> 'msg) : ('value -> Cmd<'msg>) * (System.TimeSpan option -> Async<unit>) =
333335
let rateLimit = System.TimeSpan.FromMilliseconds(float interval)
334336
let funLock = obj() // ensures safe access to resources shared across different threads
335337
let mutable lastDispatch = System.DateTime.MinValue
336338
let mutable pendingValues: 'value list = []
337339
let mutable cts: CancellationTokenSource = null // if set, allows cancelling the last issued Command
338340

341+
// gets the time to wait until the next allowed dispatch returning a negative timespan if the time is up
342+
let getTimeUntilNextDispatch () =
343+
lastDispatch.Add(rateLimit) - System.DateTime.UtcNow
344+
339345
// dispatches all pendingValues and resets them while updating lastDispatch
340346
let dispatchBatch (dispatch: 'msg -> unit) =
341347
// Dispatch in the order they were received
@@ -344,39 +350,57 @@ module Cmd =
344350
lastDispatch <- System.DateTime.UtcNow
345351
pendingValues <- []
346352

347-
// Return a factory function mapping input values to sleeping Commands dispatching all pending messages
348-
fun (value: 'value) ->
349-
[ fun dispatch ->
350-
lock funLock (fun () ->
351-
let now = System.DateTime.UtcNow
352-
let elapsedSinceLastDispatch = now - lastDispatch
353-
pendingValues <- value :: pendingValues
354-
355-
// If the interval has elapsed since the last dispatch, dispatch all pending messages
356-
if elapsedSinceLastDispatch >= rateLimit then
357-
dispatchBatch dispatch
358-
else // schedule dispatch
359-
360-
// if the the last sleeping dispatch can still be cancelled, do so
361-
if cts <> null then
362-
cts.Cancel()
363-
cts.Dispose()
364-
365-
// used to enable cancelling this dispatch if newer values come into the factory
366-
cts <- new CancellationTokenSource()
367-
368-
Async.Start(
369-
async {
370-
// wait only as long as we have to before next dispatch
371-
do! Async.Sleep(rateLimit - elapsedSinceLastDispatch)
372-
373-
lock funLock (fun () ->
374-
dispatchBatch dispatch
375-
376-
// done; invalidate own cancellation
377-
if cts <> null then
378-
cts.Dispose()
379-
cts <- null)
380-
},
381-
cts.Token
382-
)) ]
353+
// a factory function mapping input values to sleeping Commands dispatching all pending messages
354+
let factory =
355+
fun (value: 'value) ->
356+
[ fun dispatch ->
357+
lock funLock (fun () ->
358+
let untilNextDispatch = getTimeUntilNextDispatch()
359+
pendingValues <- value :: pendingValues
360+
361+
// If the interval has elapsed since the last dispatch, dispatch all pending messages
362+
if untilNextDispatch <= System.TimeSpan.Zero then
363+
dispatchBatch dispatch
364+
else // schedule dispatch
365+
366+
// if the the last sleeping dispatch can still be cancelled, do so
367+
if cts <> null then
368+
cts.Cancel()
369+
cts.Dispose()
370+
371+
// used to enable cancelling this dispatch if newer values come into the factory
372+
cts <- new CancellationTokenSource()
373+
374+
Async.Start(
375+
async {
376+
// wait only as long as we have to before next dispatch
377+
do! Async.Sleep(untilNextDispatch)
378+
379+
lock funLock (fun () ->
380+
dispatchBatch dispatch
381+
382+
// done; invalidate own cancellation
383+
if cts <> null then
384+
cts.Dispose()
385+
cts <- null)
386+
},
387+
cts.Token
388+
)) ]
389+
390+
// a function to wait until after the next async dispatch + some buffer time to ensure the dispatch is complete
391+
let awaitNextDispatch buffer =
392+
lock funLock (fun () ->
393+
async {
394+
if not pendingValues.IsEmpty then
395+
let untilAfterNextDispatch =
396+
getTimeUntilNextDispatch()
397+
+ match buffer with
398+
| Some value -> value
399+
| None -> System.TimeSpan.Zero
400+
401+
if untilAfterNextDispatch > System.TimeSpan.Zero then
402+
do! Async.Sleep(untilAfterNextDispatch)
403+
})
404+
405+
// return both the factory and the await helper
406+
factory, awaitNextDispatch

0 commit comments

Comments
 (0)