dart-lang / stream_transform

Dart utility methods to create StreamTransfomer instances to manipulate Streams

Home Page:https://pub.dev/packages/stream_transform

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[FR] exhaustMap

purplenoodlesoop opened this issue · comments

Hello folks.

Seeing that this package could potentially be merged into package:async, got me thinking about what features it lacks for my personal usage to completely replace the RxDart package.

The only one is basically the exhaustMap transformer. From the most used higher-order transformers triad asyncExpand, switchMap and exhaustMap, the last one is lacking, so it would be great to have it natively.

Thanks!

From https://rxjs.dev/api/operators/exhaustMap it looks like exhaustMap matches the semantics of asyncExpand.

Can you clarify the difference you expect from asyncExpand?

Hi @natebosch!

Thanks for considering my issue. Sure!

Yes, semantically asyncExpand and exhaustMap both represent higher-order stream mappers, but they have a key difference – while asyncExpand would sequentially flatten the streams, exhaustMap would drop all events from incoming streams if one is being processed, "flattened", at the moment.

In terms of RxJS, asyncExpand is concatMap and exhaustMap is, well, exhaustMap.

RxDart also has exhaustMap, and its description reads as follows:

Converts items from the source stream into a Stream using a given mapper. It ignores all items from the source stream until the new stream completes.

Ah interesting so exhaustMap -> asyncExpand the way that asyncMapSample -> asyncMap. Thanks for clarifying, I had misread the RxJS docs when I looked at this.

I think the feature makes sense. I'm tempted to name it asyncExpandSample, parity with names from Rx was never a priority, and it may be easier to understand with that name. At the same time, now that I understand the behavior exhaustMap does make sense to me. @jakemac53 @munificent - what is your opinion on the naming?

asyncExpandSample makes more sense to me

Agree on the naming, asyncExpandSample sounds right and consistent with asyncMapSample. Thanks again, thrilled to see this feature as part of the stream_transform!

I discovered there is an edge of the behavior that differs from asyncMapSample.

In asyncMapSample we only drop events if more than one comes in during the waiting period. If only one event comes in while work is ongoing, it will be held and delivered after the work.

With the exhaustMap reference all events that come in during the waiting period are dropped.

|--A--B--C--------| # Events
|--AAAAABBBBBCCCCC| #Ongoing work

vs

|--A--B--C----| #Events
|--AAAAA-CCCCC| #Ongoing Work

(I even found an implementation called exhaustMap which is asyncMapSample with the behavior of dropping all events during ongoing work, so that's clearly a useful behavior too)

So maybe we should go with exhastMap, or think of some other name were we could have a similar implementation for asyncMap. We could easily deliver asyncMapSample with the same "sampling" semantics as well, but it might be better to hold off for a use case.

@purplenoodlesoop - to confirm - are you interested in the behavior were all events during an ongoing stream are ignored, or where we drop all but the most recent?

Hmm, interesting, there is indeed an edge. @natebosch I'm interested in dropping all events during ongoing work, so exhaustMap's behavior it is. To be honest, I'm not really sure where asyncMapSample-like behavior would benefit; I don't see a common use case for it.

To be honest, I'm not really sure where asyncMapSample-like behavior would benefit; I don't see a common use case for it.

It still limits how often you would actually process events, but ensures that if you had any events while processing the last one you will still process the most recent one. So lets say for change detection or something, you were still processing the last change detection cycle and don't want to interrupt that with new events, but you do need to schedule the next round of change detection to run immediately after processing the current round, if events came in during it.

I hope I'm not confusing myself here...

Another analogy to consider is our throttle method.

Compared to ReactiveX, our throttle is like throttleFirst except we also support the trailing argument like throttleTime from RxJs.

Within stream_transform throttle(trailing: true) has a similar behavior to asyncMapSample. Throttle takes a single Duration, while sample uses the duration of the Future returned by the callback. If we add an option for asyncMapSample(trailing: false) and also in our supposed asyncExpandSample, where trailing has the same meaning as it does in throttle, then I think exhaustMap would correspond to asyncExpandSample(trailing: false).

One slight downside is that the default value of the leading: argument would be different between "sample" and "throttle" variants.

One potential upside is that the generality, and especially using the generality as the way to frame discussion of behavior comparisons and contrasts between the methods, could still make this all easier to understand than having more unique names like exhuast vs *Sample.

We'd be adding at least one use case pre-emptively (no one has asked for asyncExpandSample(trailing: true) as far as I know) but we'd also be satisfying another use case for which at least one internal team has their own implementation (asyncMapSample(trailing: false)).