Merge main Observable stream with updates - rxjs

I need to merge main stream with updates stream this way:
Main: ----A-------B-------------C---|--
Upd: -D-----E---------F---G--------|--
========================================
Result:----A--AE---B----BF--BG---C---|--
I.e., when Main emitted, the result should always be Main (or Main with empty Upd). When Upd was emitted without previous Main, it should be ignored. If Upd was emitted after Main, then they should be combined.
Consider this TypeScript code:
interface Item {
Id: number;
Data: string;
}
function mergeUpdates(main: Item[], upd: Item[]) {
if (!upd || upd.length === 0) {
return main;
}
const result = main;
// const result = {...main};
for (const updatedItem of upd) {
const srcIndex = result.findIndex(_ => _.Id === updatedItem.Id);
if (srcIndex >= 0) {
result[srcIndex] = updatedItem;
} else {
result.push(updatedItem);
}
}
return result;
}
const main$ = new Subject<Item[]>();
const upd$ = new Subject<Item[]>();
const result$ = combineLatest(main$, upd$).pipe( // combineLatest is wrong operator!
map(([main, upd]) => mergeUpdates(main, upd)));
$result.subscribe(r => console.log(r.map(_ => _.Data).join(',')));
main$.next([{Id:1, Data:'Data1'}, {Id:2, Data:'Data2'}]);
upd$.next([{Id:1, Data:'Updated1'}]);
upd$.next([{Id:1, Data:'Updated2'}]);
main$.next([{Id:1, Data:'Data1_Orig'}, {Id:2, Data:'Data2'}]);
// Expexted result:
// 'Data1,Data2'
// 'Updated1,Data2'
// 'Updated2,Data2'
// 'Data1_Orig,Data2'
The only solution I have in mind is to use 'combineLatest' and mark items in upd$ stream as processed, thus do not use it again when data from main$ emitted later. I believe this is not the best approach as it cause unwanted side effects.
Is there any better solution for this task?
Thank you in advance.

Here would be my approach:
main$.pipe(
switchMap(
mainValue => merge(
of(mainValue),
upd$.pipe(
map(updVal => mainValue + updVal)
)
)
)
)
switchMap - make sure the inner observable's emitted values will be combined with the latest outer value
merge(of(a), upd$.pipe()) - emit the main value first, then listen to any notifications upd$ emits and combine them with the current main value
If another outer value comes in, the inner subscriber will be unsubscribed, meaning that the upd subject won't have redundant subscribers.

Related

Use RxJs operator like merge but keep track of source observables in the result?

I want to combine observables as is done with the "merge" operator but I still want to be able to know which input observable emitted, is there a way to do that?
For example:
private result$ = merge(this.obs1$, this.obs2$).pipe(
scan((result, change) => index + change, 0),
shareReplay(1)
);
Here both values from obs1 and obs2 will go into the "change" variable in the scan function whenever any of the input observables emit, but if I had access to a projector function where I could mark the values from the input observables with different names I could then do different things in the following scan function depending on which input observable emitted. Other operators like CombineLatest or ForkJoin do not seem to be applicable here either as they require completion or emits from all input observables.
If you need to keep track of which input observable emitted, then you may need to add metadata to your source observable. Without knowing the context of how result$ is used, this is the best possible solution with the information given.
I would suggest adding an id property to each observable you need to keep track of. Then, you can use some strategy in your scan operator based on the ID.
Below is a simple example using an id for each source observable. In the scan operator you will see how my strategy changes based on the ID.
import { interval, merge, of } from "rxjs";
import { map, scan, shareReplay } from "rxjs/operators";
const obs1$ = interval(1000).pipe(map(i => ({ i, id: "obs1" })));
const obs2$ = interval(3000).pipe(map(i => ({ i, id: "obs2" })));
let index = 0;
const result$ = merge(obs1$, obs2$).pipe(
scan((result, change) => {
if (change.id === "obs1") {
return index + change.i;
}
if (change.id === "obs2") {
return index + change.i * 2;
}
}, 0),
shareReplay(1)
);
result$.subscribe(console.log);
https://stackblitz.com/edit/rxjs-as5ket
The library #react-rxjs/utils has a util named mergeWithKey that can be used like this:
import { Subject } from "rxjs"
import { scan, startWith } from 'rxjs/operators'
import { mergeWithKey } from '#react-rxjs/utils'
const inc$ = new Subject()
const dec$ = new Subject()
const resetTo$ = new Subject<number>()
const counter$ = mergeWithKey({
inc$,
dec$,
resetTo$,
}).pipe(
scan((acc, current) => {
switch (current.type) {
case "inc$":
return acc + 1
case "dec$":
return acc - 1
case "resetTo$":
return current.payload
default:
return acc
}
}, 0),
startWith(0),
)
The implementation is pretty straight-forward:
import { merge, Observable, ObservableInput, from, SchedulerLike } from "rxjs"
import { map } from "rxjs/operators"
/**
* Emits the values from all the streams of the provided object, in a result
* which provides the key of the stream of that emission.
*
* #param input object of streams
*/
export const mergeWithKey: <
O extends { [P in keyof any]: ObservableInput<any> },
OT extends {
[K in keyof O]: O[K] extends ObservableInput<infer V>
? { type: K; payload: V }
: unknown
}
>(
x: O,
concurrent?: number,
scheduler?: SchedulerLike,
) => Observable<OT[keyof O]> = (input, ...optionalArgs) =>
merge(
...(Object.entries(input)
.map(
([type, stream]) =>
from(stream).pipe(
map((payload) => ({ type, payload } as any)),
) as any,
)
.concat(optionalArgs) as any[]),
)
Is this what you needed?

BufferTime with leading option

I have some events that I'd like to buffer but I'd like to buffer only after the first element.
[------bufferTime------]
Input over time:
[1, 2, 3, -------------|---4, 5, 6 ----------------]
Output over time:
[1]-----------------[2,3]---[4]------------------[5,6]
is there a way to do this?
I think this can be solved by dividing your stream into two, firstValue$ and afterFirstValue$, and then merging them.
import { merge } from 'rxjs';
import { take, skip, bufferTime } from 'rxjs/operators';
...
firstValue$ = source$.pipe(
take(1)
);
afterFirstValue$ = source$.pipe(
skip(1),
bufferTime(5000)
);
merge(firstValue$, afterFirstValue$)
.subscribe(result => {
// Do something
});
Answer to follow up question concerning subject
So I have done it so that the original source is a subject here. It is not exactly how you described it, but I think maybe this is what you want.
import { merge, Subject } from 'rxjs';
import { take, skip, bufferTime } from 'rxjs/operators';
import { Source } from 'webpack-sources';
...
source$ = new Subject();
firstValue$ = source$.pipe(
take(1)
);
afterFirstValue$ = source$.pipe(
skip(1),
bufferTime(5000)
);
merge(firstValue$, afterFirstValue$)
.subscribe(result => {
// Do something
});
source$.next(1);
source$.next(1);
source$.next(1);
You can use multicast to split the stream into two and just pass the first value through.
import { concat, Subject } from “rxjs”;
import { multicast, take, bufferCount } from “rxjs/operators”;
source.pipe(
multicast(
new Subject(),
s => concat(
s.pipe(take(1)),
s.pipe(bufferCount(X)),
)
),
);
I got really good answers that enlightened my view of the problem and made me come up with the real thing that I was needing, that was something like this:
function getLeadingBufferSubject (bufferTimeArg) {
const source = new Subject()
const result = new Subject()
let didOutputLeading = false
const buffered$ = source
.pipe(bufferTime(bufferTimeArg))
.pipe(filter(ar => ar.length > 0))
.pipe(map(ar => [...new Set(ar)]))
buffered$.subscribe(v => {
didOutputLeading = false
const slicedArray = v.slice(1)
// emits buffered values (except the first) and set flag to false
if (.length > 0) result.next(v.slice(1))
})
// emits first value if buffer is empty
source.subscribe(v => {
if (!didOutputLeading) {
didOutputLeading = true
result.next(v)
}
})
// call .next(value) on "source"
// subscribe for results on "result"
return {
source,
result
}
}
I had the same problem and after playing around with it, I found this additional solution:
source$.pipe(
buffer(source$.pipe(
throttleTime(bufferTime, asyncScheduler, {leading: true, trailing: true}),
delay(10) // <-- This here bugs me like crazy though!
)
)
Because throttle already features a leading option, you can just use it to trigger buffer emits manually.
I would really like to get rid of that delay here though. This is necessary because the inner observable is triggered first causing the buffer to emit prematurely.

Rxjs zip-like operator which returns only when observable fires in specific order

Let's say I have two Observables A and B, and I want to combine them to produce this behaviour: the subscription combination returns only if A fires when a B has already been fired. It differs from the zip because I don't want any return if A has already been fired and then a B fires. In other words: ignores A until B fires then return on the next A, then ignore any other A until B fires ... so on
I believe you need withLatestFrom operator:
import { withLatestFrom, map } from 'rxjs/operators';
import { interval } from 'rxjs';
const obsA = interval(2000);
const obsB = interval(1000);
const resultingObs = obsB.pipe(
withLatestFrom(obsA),
map(([bValue, aValue]) => {
return aValue
})
);
// This should emit values from obsA only when obsB has been fired.
const subscribe = resultingObs .subscribe(val => console.log(val));

RxJs emit after delay or next event on the stream

I've pairs of events: add1/add2/etc and remove1/remove2/etc. I'd like the following:
when an add1 is emitted on the stream
if DELAY transpires with no new add* emissions
emit remove1
if add* is emitted
emit remove1 for add1 immediately
emit remove* for add* after DELAY
This should continue for all emissions of add* on the stream.
Here's a test I've written using RxJS marble testing for this case:
import test from 'tape'
import { set, lensPath } from 'ramda'
import { TestScheduler } from 'rxjs/testing'
import hideAfterDelay from '../another/file'
import { actionCreators } from '../another/dir'
const prefix = 'epics -> notifications'
test(`${prefix} -> hideAfterDelay`, t => {
t.plan(1)
const scheduler = new TestScheduler(t.deepEqual)
const actionMap = {
a: createAddAction('hello!'),
b: createAddAction('goodbye!'),
x: actionCreators.notifications.remove('hello!'),
y: actionCreators.notifications.remove('goodbye!')
}
scheduler.run(({ cold, expectObservable }) => {
const actionStream = cold('a-------a-b-a------', actionMap)
const expected = '-----x-----x-y----x'
const actual = hideAfterDelay(5)(actionStream)
expectObservable(actual).toBe(expected, actionMap)
})
})
function createAddAction (name) {
const action = actionCreators.notifications.add(name)
const lens = lensPath(['payload', 'id'])
return set(lens, name, action)
}
I think the test is representative of the behavior I described above and that I want.
How can I write this observable? I've tried using timer and race but I haven't been able to get this working...
This is an epic using redux-observable, btw.
Using RxJS v6
Ok, I think I got a working solution using a closure and slightly modifying my test assertion.
First, the expected marble diagram should look like this
// input: a-------a-b-a------
// - expected: -----x-----x-y----x
// + expected: -----x----x-y----x
//
// Note above that the middle x and y emit at the same time as new
// `add*` actions on the source stream instead of one frame later
With that small change—which still feels consistent with my description in the question—I was able to get my test passing with the following:
import { of, timer, empty } from 'rxjs'
import { switchMap, mapTo, tap, merge } from 'rxjs/operators'
import { ofType } from '../operators'
import actionTypes from '../../actionTypes/notifications'
import { actionCreators } from '../..'
export default (delay = 3000) => actionStream => {
let immediateRemove
return actionStream.pipe(
ofType(actionTypes.ADD),
switchMap(action => {
let obs = empty()
if (immediateRemove) {
obs = of(immediateRemove)
}
const remove = actionCreators.notifications.remove(action.payload.id)
immediateRemove = remove
return obs.pipe(
merge(
timer(delay).pipe(
tap(() => {
immediateRemove = null
}),
mapTo(remove)
)
)
)
})
)
}
I've no idea if this is the best or right way to solve it, but I'm fairly certain it is a way.

Operator that skips the next emission from the source whenever another Observable emits

I have a use case where I need an Observable to skip its next emission whenever another notifier Observable emits.
source: |---X---X---X---X---X---X---X---X---X---X--|>
notifier: |-------------X---------X----------X-------|>
result: |---X---X---X-------X---X-------X-------X--|>
Basically, I want an operator called skipNextWhen that takes in the notifier observable and skips the next emission from the source.
I tried using an implementation that uses the pausable operator (re-implemented using switchMap), but couldn't get it to work.
pausable.ts
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
import 'rxjs/add/observable/never';
import 'rxjs/add/operator/startWith';
declare module 'rxjs/Observable' {
interface Observable<T> {
pausable: typeof pausable;
}
}
function pausable<T>(notifier: Observable<boolean>): Observable<T> {
return notifier.startWith(false).switchMap((paused) => {
if (paused) {
return Observable.never();
} else {
const source = new Subject();
this.subscribe(source);
return source;
}
});
}
Observable.prototype.pausable = pausable;
skipNextWhen.ts
import { Observable } from 'rxjs/Observable';
import './pausable';
declare module 'rxjs/Observable' {
interface Observable<T> {
skipNextWhen: typeof skipNextWhen;
}
}
function skipNextWhen<T, R>(other: Observable<T>): Observable<R> {
const notifier = Observable.merge(this.map(() => false),
other.map(() => true));
return this.pausable(notifier);
}
Observable.prototype.skipNextWhen = skipNextWhen;
Is there a more suitable operator that I should consider using instead? The behavior I'm seeing with my current implementation is that the result Observable emits once, and then never again - even if the notifier Observable never emits.
I can think of two solutions to this:
Using .filter(), .do() and a few side-effects.
This is mayne easier to understand solution even though it's not that "Rx" way:
function skipNextWhen(other) {
let skipNext = false;
return this.merge(other.do(() => skipNext = true).filter(() => false))
.filter(val => {
const doSkip = skipNext;
skipNext = false;
return !doSkip;
});
}
I'm using merge() just to update skipNext, other's value is always ignored.
Using .scan():
This solution is without any state variables and side-effects.
function skipNextWhen(other) {
const SKIP = 'skip';
return this.merge(other.mapTo(SKIP))
.scan((acc, val) => {
if (acc === SKIP) {
return null;
} else if (val === SKIP) {
return SKIP;
} else {
return val;
}
}, [])
.filter(val => Boolean(val) && val !== SKIP);
}
Basically, when SKIP arrives I return it right away because it's going to be passed again in acc parameter by the scan() operator and later ignored by filter().
If I receive a normal value but the previous value was SKIP I ignore it and return just null which is later filter away.
Both solutions give the same result:
Observable.prototype.skipNextWhen = skipNextWhen;
const source = Observable.range(1, 10)
.concatMap(val => Observable.of(val).delay(100));
source
.skipNextWhen(Observable.interval(350))
.subscribe(console.log);
This prints the following:
1
2
3
5
6
8
9
10
Just be aware that you're not in fact creating new operator. You just have a shortcut for an operator chain. This for example doesn't let you unsubscribe from other when the source completes.
I've started a (very) small library of some rxjs utils I've wanted. It happens to have a function to do exactly what you ask: skipAfter. From the docs:
source: -1-----2-----3-----4-----5-|
skip$: ----0----------0-0----------
result: -1-----------3-----------5-|
The library is here: https://github.com/simontonsoftware/s-rxjs-utils

Resources