Ignore events on stream until condition is satisfied? - rxjs

I created a stream from button click events. The button corresponds to a create action on the database. Obviously, I want the database action to fire only once (at least until it completes). Is there a way to ignore events on createButtonStream until Api.create returns? i.e., the first event should call #Api.create, subsequent events should be ignored until #Api.create returns.
createButtonStream
.flatMap(() => Api.create()) //Needs to fire once until doSomething() is called
.onValue(result => doSomething(result))
The only way that comes to mind is to use global state...and I'd rather not do that.
//i don't wanna do this
let condition = true
createButtonStream
.filter(() => condition)
.map(() => condition = false)
.flatMap(() => Api.create())
.onValue(result => {condition = true; doSomething(result)})

In RxJS you use the flatMapFirst or exhaustMap operator (if using RxJS 5)
createButtonStream
.flatMapFirst(() => Api.create())
.subscribe(result => doSomething(result));
flatMapFirst will silently drop events if they arrive before the first source completes, the method should not get invoked.

You can use awaiting. You need a Bus to work around the chicken and egg problem.
const filterBus = new Bacon.Bus()
const createDone = createButtonStream
.filter(filterBus.toProperty(true))
.flatMap(() => Api.create())
const inProgress = createButtonStream.awaiting(createDone)
filterBus.plug(inProgress.not())
createDone.onValue(doSomething)

Related

Is it possible to write an rxjs operator that controls subscription to it's source?

Let's say I have an observable source that has the following properties:
It makes a network request the first time it's subscribed to
It's idempotent, so it will always emit the same value after it's first subscribed to
It's low priority, so we don6t6 want to be too eager in subscribing to it
What would be ideal is if there were some sort of operator delaySubscriptionUntil that would delay subscription until some other observable s emits a value.
So for example:
const s = new Subject<void>();
const l = source
.pipe(
delaySubscriptionUntil(s));
l.subscribe(console.log);
// The above won't print anything until this line executes
s.next();
I looked through the documentation to see if there's an existing operator like this, but haven't found one.
You just put the subject first and switchMap
const l = s.pipe(
switchMap(() => source)
);
Once the subject emits then the source will be subscribed to.
Any thing that is after wont work as it relies on the previous observable emitting a value. You can have a filter in the chain that stops the previous observable's emission being emitted but there is nothing you can pass back up the chain to control outer subscriptions.
You could use a takeWhile
let allow = false;
const l = source.pipe(
takeWhile(allow)
);
but here the subscription to source is active, it is emitting values, they are just stopped from being passed through.
So you could make a similar operator that keeps an internal flag and is flipped by a subject but source is still going to be emitting, you are just filtering values. You could buffer up the values if you don't want to lose them.
You could use share() which will share the result of anything that happened before it until you call sub.next() with a new url then the request will happen again.
const sub = new BehaviorSubject<string>('http://example.com/api');
const result$ = sub.pipe(
exhaustMap(url => this.http.get(url)),
share()
)
// Each one of these subscriptions will share the result.
// The http request will be called only once
// until you send a new url to the BehaviorSubject.
result$.subscribe(val => console.log(val));
result$.subscribe(val => console.log(val));
result$.subscribe(val => console.log(val));
result$.subscribe(val => console.log(val));

Repeat on an BehaviorSubject

I want to reemit the last value of my observable at a fix interval, to I tried
obs.pipe(repeat({delay:1000})).subscribe(x => console.log('Emitted', x));
but it did not work. after looking into this, my observable is in fact a BehaviorSubject.
So my Question is Why does the 1st emits every second
of('Observable').pipe(repeat({ delay: 1000 })).subscribe(x => console.log(x));
but not the this?
var bs = new BehaviorSubject('BehaviorSubject');
bs.pipe(repeat({ delay: 1000 })).subscribe(x => console.log(x));
How to do it with my BehaviorSubject?
Edit
And I would also like to reset my timer when the subject emits a new value.
the solution I found is
var bs = new BehaviorSubject('BehaviorSubject');
bs.pipe(switchMap(x => timer(0,1000).pipe(map => () => x)).subscribe(x => console.log(x));
but it feels ugly.
You can derive an observable from your BehaviorSubject that switchMaps to a timer that emits the received value. Whenever the subject emits, the timer is reset and will emit the latest value:
const bs = new BehaviorSubject('initial value');
const repeated = bs.pipe(
switchMap(val => timer(0, 1000).pipe(
map(() => val)
))
);
Here's a StackBlitz demo.
So my Question is Why does the 1st emits every second, but not the this?
The reason your example code using of as the source works and not the code using the BehaviorSubject can be found in the documentation of the repeat operator:
Returns an Observable that will resubscribe to the source stream when the source stream completes.
The observable created using of completes after it emits the provided value, so it will resubscribe. Since the BehaviorSubject was not completed, it will not resubscribe.

Cancel repeated subscription in mergeMap

How to combine the distinct, switchMap and mergeMap operators, so that when the source emits repeated values (detected by distinct.keySelector), the previous subscription is canceled (as in the switchMap), but if the value is not repeated follow the behavior of mergeMap?
Example:
source = from(1, 2, 1, 2, 3) // 'abcde'
result = source.pipe(delay(), combination() // '--cde'
I'm currently doing something like:
const activeSubscriptions = new Map();
source$.pipe(
mergeMap((value) => {
const pendingSubscription = activeSubscriptions.get(value);
if (pendingSubscription) {
pendingSubscription.unsubscribe();
activeSubscriptions.delete(value);
}
const request$ = new Subject();
const subscription = this.service.get(value).subscribe({
complete: () => request$.complete(),
error: (err) => request$.error(err),
next: (value) => request$.next(value),
});
activeSubscriptions.set(value, subscription);
return request$;
})
);
But looking for a better way to do that.
Thank you in advance
I think you can use the windowToggle operator for this:
src$ = src$.pipe(shareReplay(1));
src$.pipe(
ignoreElements(),
windowToggle(src$.pipe(observeOn(asyncScheduler)), openValue => src$.pipe(skip(1), filter(v => v === openValue))),
mergeMap(
window => window.pipe(
startWith(null),
withLatestFrom(src$.pipe(take(1))),
map(([, windowVal]) => windowVal),
)
),
)
A replacement for observeOn(asyncScheduler) could also be delay(0), the important thing is to make sure the order in which the src$'s subscribers receive the value is correct. In this case, we want to make sure that when src$ emits, the clean-up takes place first, so that's why we're using src$.pipe(observeOn(asyncScheduler)).
ignoreElements() is used because each window is paired to only one value, the one which has created the window. The first argument(s) passed to windowToggle will describe the observable(s) which can create the windows. So, we only need those, since we're able to get the last value with the help of
window => window.pipe(
startWith(null),
withLatestFrom(src$.pipe(take(1))),
map(([, windowVal]) => windowVal),
)
By the way, a window is nothing but a Subject.
Lastly, if you want to perform async operations inside the window's pipe, you'll have to make sure that everything is unsubscribed when the window is completed(closed). To do that, you could try this:
window => window.pipe(
startWith(null),
withLatestFrom(src$.pipe(take(1))),
map(([, windowVal]) => windowVal),
switchMap(val => /* some async action which uses `val` */),
takeUntil(window.pipe(isEmpty()))
)
where isEmpty will emit either true or false when the source(in this case, the window) completes. false means that the source had emitted at least one value before emitting a complete notification, and true otherwise. In this case, I'd say it's irrelevant whether it's true or false, since the window will not emit any values by itself(because we have used ignoreElements, which ignores everything except error and complete notifications).

RxJS mergeMap doesn't behave as expected

I have this piece of RxJS code
this.listItems$ = this.store.select(EntityState.relationshipItems).pipe(
map(fn => fn(12)),
mergeMap(items => items),
map(this.toListItem),
toArray<ListItem>(),
tap(x => console.log(x))
);
Using mergeMap(items => items) I'm trying to "flatten" the array, then map each item to another object, and then convert it back to an array.
However, the flow doesn't even reach the last tap. I can see the toListItem function is called, but I don't understand why it stops there.
Transforming it to
this.listItems$ = this.store.select(EntityState.relationshipItems).pipe(
map(fn => fn(12)),
map(items => items.map(this.toListItem)),
tap(x => console.log(x))
);
makes it work, but I'd like to understand why the above one doesn't work.
That's because this.store.select(...) is a Subject that never completes (if it did then you could select data just once which doesn't make sense).
However, toArray collects all emissions from its source and when its source completes it emits a single array. But the source is this.store.select(...) that never completes so toArray never emits anything.
So probably the easiest workaround would be just restructuring your chain:
this.listItems$ = this.store.select(EntityState.relationshipItems).pipe(
map(fn => fn(12)),
mergeMap(items => from(items).pipe(
map(this.toListItem),
toArray<ListItem>(),
tap(x => console.log(x))
)),
);
Now the source is from that completes after iterating items so toArray will receive complete notification and emit its content as well.

Returning source observable value after inner observable emits value

Within an observable chain, I need to perform some async work, then return the source value to the next observable so I had to pipe(mapTo(x)) after the async work.
A more complete example:
// fake async work with just 1 null value
someAsyncWork = () => of(null)
of('1', '2', '3').pipe(
// some async work
concatMap(no => someAsyncWork().pipe(mapTo(no))),
concatMap(no => `Some async work [${no}] done!`)
).subscribe(message => console.log(message))
I cannot use tap(no => someAsyncWork()) because that would cause the next observable to run before someAsyncWork() returns.
While my current approach works, it somewhat clutters the code...and I have this pattern repeated in many places within the codebase.
Question: Anyway to do this without pipe(mapTo(no)) - in a more concise/readable way?
Perhaps the simplest thing to do would be to write your own pipeable operator.
For example:
const concatTap = <T>(project: (value: T) => Observable<any>) =>
concatMap((value: T) => project(value).pipe(mapTo(value)));
However, that assumes the observable for the async operation emits only a single value. To guard against multiple values being emitted you could do something like this:
const concatTap = <T>(project: (value: T) => Observable<any>) =>
concatMap((value: T) => concat(project(value).pipe(ignoreElements()), of(value)));
You could use concatTap like this:
of('1', '2', '3').pipe(
concatTap(() => someAsyncWork()),
concatMap(no => `Some async work [${no}] done!`)
).subscribe(message => console.log(message));
I'm sure you could choose a better name than I did. concatTap was the first thing that popped into my head. Naming things is hard.

Resources