@@ -8,6 +8,8 @@ import cats.effect.Temporal
88import cats .effect .syntax .all .*
99import cats .syntax .all .*
1010import cats .~>
11+ import monocle .Focus
12+ import monocle .Lens
1113
1214import scala .concurrent .duration .FiniteDuration
1315
@@ -28,39 +30,40 @@ import scala.concurrent.duration.FiniteDuration
2830 * G The async effect. This will be used for concurrency.
2931 */
3032final class ViewThrottlerF [F [_], G [_], A ] private (
31- waitUntil : Ref [G , FiniteDuration ],
32- nextUpdate : Ref [G , Option [ViewThrottlerF .ModCBType [G , A ]]],
33+ state : Ref [G , ViewThrottlerF .State [G , A ]],
3334 timeout : FiniteDuration ,
3435 syncToAsync : F ~> G ,
3536 dispatchAsync : G [Unit ] => F [Unit ]
3637)(using G : Temporal [G ]) {
38+ import ViewThrottlerF .State
3739
3840 private def throttle : F [Unit ] =
3941 dispatchAsync :
40- G .monotonic.flatMap(now => waitUntil.set (now + timeout))
42+ G .monotonic.flatMap(now => state.update( State . waitUntil[ G , A ].replace (now + timeout) ))
4143
4244 private def throttlerView (view : ViewF [F , A ]): ViewF [F , A ] =
4345 view.withOnMod(_ => throttle)
4446
4547 private def attemptSet (modCB : (A => A , (A , A ) => G [Unit ]) => G [Unit ]): G [Unit ] =
46- (waitUntil.get, G .monotonic).flatMapN: (waitUntil, now) =>
47- if (waitUntil > now)
48- (G .sleep(waitUntil - now) >> attemptSet(modCB)).start.void
49- else
50- nextUpdate
51- .flatModify: accumModCb =>
52- (none, accumModCb.map((mod, cb) => modCB(mod, cb)).getOrElse(G .unit))
53- .flatTap(_ => G .unit)
48+ G .monotonic.flatMap: now =>
49+ state
50+ .modify: s =>
51+ if (s.waitUntil > now)
52+ (s, (G .sleep(s.waitUntil - now) >> attemptSet(modCB)).start.void)
53+ else
54+ (State .nextUpdate[G , A ].replace(none)(s),
55+ s.nextUpdate.map((mod, cb) => modCB(mod, cb)).getOrElse(G .unit)
56+ )
57+ .flatten
5458
5559 private def throttledView (view : ViewF [F , A ]): ViewF [G , A ] =
5660 new ViewF [G , A ](
5761 get = view.get,
5862 modCB = (mod, cb) =>
59- nextUpdate.update: x =>
60- x match
61- case None => (mod, cb).some
62- case Some (oldMod, _) => (oldMod.andThen(mod), cb).some // We only keep last CB
63- >>
63+ state.update(State .nextUpdate[G , A ].modify {
64+ case None => (mod, cb).some
65+ case Some (oldMod, _) => (oldMod.andThen(mod), cb).some // We only keep last CB
66+ }) >>
6467 attemptSet : (newMod, newCB) =>
6568 syncToAsync(view.modCB(newMod, (oldA, newA) => dispatchAsync(newCB(oldA, newA))))
6669 )
@@ -70,13 +73,25 @@ final class ViewThrottlerF[F[_], G[_], A] private (
7073}
7174
7275object ViewThrottlerF {
76+ // The type of ViewF.modCB's parameters, to avoid repeating everywhere.
7377 private type ModCBType [F [_], A ] = (A => A , (A , A ) => F [Unit ])
7478
79+ // `waitUntil` (pause deadline) and `nextUpdate` (last accumulated update) are kept in a single
80+ // `Ref` so that, in `attemptSet`, deciding whether the pause has elapsed and taking the pending
81+ // update happen as one atomic transition.
82+ private case class State [G [_], A ](waitUntil : FiniteDuration , nextUpdate : Option [ModCBType [G , A ]])
83+ private object State :
84+ def waitUntil [G [_], A ]: Lens [State [G , A ], FiniteDuration ] =
85+ Focus [State [G , A ]](_.waitUntil)
86+ def nextUpdate [G [_], A ]: Lens [State [G , A ], Option [ModCBType [G , A ]]] =
87+ Focus [State [G , A ]](_.nextUpdate)
88+
7589 def apply [F [_], G [_], A ](
7690 timeout : FiniteDuration ,
7791 syncToAsync : F ~> G ,
7892 dispatchAsync : G [Unit ] => F [Unit ]
7993 )(using G : Temporal [G ]): G [ViewThrottlerF [F , G , A ]] =
80- (G .monotonic.flatMap(G .ref(_)), G .ref(none[ModCBType [G , A ]]))
81- .mapN(new ViewThrottlerF (_, _, timeout, syncToAsync, dispatchAsync))
94+ G .monotonic
95+ .flatMap(now => G .ref(State [G , A ](now, none)))
96+ .map(new ViewThrottlerF (_, timeout, syncToAsync, dispatchAsync))
8297}
0 commit comments