Should we avoid nested rxjs operators? One case which I cannot test - rxjs

I have written the following effect in my Angular app which uses rxjs. On MyActions.myAction, I receive an object containing a property ids - an array of ids - and for each id I want to send an HTTP request via this.myApiService.getResource, which returns an Observable<Resource>. I want then to collect all results in an array, and dispatch another action passing the array.
public loadResources$: Observable<MyAction> = this.actions$.pipe(
ofType(MyActions.myAction),
switchMap(({ ids }) => from(ids).pipe(
mergeMap(id => this.myApiService.getResource(id)),
toArray()
)),
map(resources) => MyActions.resourcesLoaded({ resources } )),
);
The code above does the job, but I wonder whether I should avoid nesting two flows of reactive operators, and whether there is a better way to write that.
The reason I wonder that is that I am having problems writing a test for it. I wrote the test below but I cannot make it pass.
it('should dispatch an resourcesLoaded action with the resources', () => {
const ids = ['5f7c723832758b859bd8f866'];
const resources = [{} as Resource];
const values = {
l: MyActions.loadResources({ ids }),
t: ids[0],
o: MyActions.resourcesLoaded({ resources })
};
actions = hot('--l------------', values);
const get$ = cold(' -------t-----', values);
const expected = cold('---------o-----', values);
myApiService.getResource.withArgs(ids[0]).returns(get$);
expect(myEffects.loadResources$).toBeObservable(expected);
});
The error I get is:
Expected $.length = 0 to equal 1.
Expected $[0] = undefined to equal Object({ frame: 50, notification: Notification({ kind: 'N', value: { ....
Error: Expected $.length = 0 to equal 1.
Expected $[0] = undefined to equal Object({ frame: 50, notification: Notification({ kind: 'N', value: { ....
at <Jasmine>
at compare (http://localhost:9876/Users/jacopolanzoni/Documents/Development/myProject/node_modules/jasmine-marbles/index.js:91:1)
at <Jasmine>

but I wonder whether I should avoid nesting two flows of reactive operators, and whether there is a better way to write that
I'd say it depends on what you want to achieve, at least in this case.
of([1,2,3]).pipe(mergeAll(), switchMap(value => http.get(...)))
differs from
of([1,2,3]).pipe(switchMap(ids => from(ids).pipe(mergeMap(...))))
In the first scenario, each inner observable will be discarded by the next value(except for the last value), so only 3 will resolve.
In the second scenario, it will process all of them, because you explode the array in the inner observable(which is managed by swtichMap, so the only way its inner observable will be discarded is if a new outer value(e.g another array of ids) is emitted by the source).
A case where nesting is not necessary is:
of([1,2,3])
.pipe(
// whenever you want to explode an array,
// it does not matter which higher order operator you use
// since the operation is **synchronous**
// so, `mergeAll`, `concatAll`, `switchAll` should work the same
mergeAll(),
mergeAll(id => this.apiService.useId(id))
)
// same as
of([1,2,3])
.pipe(
mergeMap(ids => from(ids).pipe(mergeMap(id => this.apiService.useId(id))))
)
As you can see, in this case, switchMap has been replaced with mergeMap.

I have found out my test was failing because toArray was waiting for the observable returned by getResource (i.e., httpClient.get) to complete. Replacing t with (t|) fixes the test:
it('should dispatch an resourcesLoaded action with the resources', () => {
const ids = ['5f7c723832758b859bd8f866'];
const resources = [{} as Resource];
const values = {
l: MyActions.loadResources({ ids }),
t: ids[0],
o: MyActions.resourcesLoaded({ resources })
};
actions = hot('--l------------', values);
const get$ = cold(' -------(t|)-----', values);
const expected = cold('---------o-----', values);
myApiService.getResource.withArgs(ids[0]).returns(get$);
expect(myEffects.loadResources$).toBeObservable(expected);
});
Yet, the first part of my question, i.e. whether it's good practice or not to nest operators like that, still stands.

Related

Using RxJS to manipulate a stream of items to obtain an array of streamed items

i'm kinda new to rxjs and can't get my head around this problem:
I have two streams:
one with incoming objects
---a----b----c----d----->
one with the selected object from a list
-------------------c---->
From the incoming objects stream make a stream of the list of objects (with scan operator)
incoming: ----a--------b-------c----------d----------------\>
list: -------[a]----[a,b]----[a,b,c]----[a,b,c,d]---------\>
When a list object is selected (n), start a new stream
the first value of the new stream is the last value of the list sliced ( list.slice(n))
incoming: ----a--------b-------c----------d--------------------e-------->
list: -------[a]----[a,b]----[a,b,c]----[a,b,c,d]--------->
selected object: ---------------------------------c------->
new stream of list: ------[c,d]-----[c,d,e]--->
i can't get the last value of the list stream when the object is selected,,,
made a marble diagram for better understanding,
selectedObject$ = new BehaviorSubject(0);
incomingObjects$ = new Subject();
list$ = incomingObjects$.pipe(
scan((acc, val) => {
acc.push(val);
return acc;
}, [])
)
newList$ = selectedObject$.pipe(
withLastFrom(list$),
switchMap(([index,list])=> incomingObjects$.pipe(
scan((acc, val) => {
acc.push(val);
return acc;
}, list.slice(index))
))
)
A common pattern I use along with the scan operator is passing reducer functions instead of values to scan so that the current value can be used in the update operation. In this case you can link the two observables with a merge operator and map their values to functions that are appropriate - either adding to a list, or slicing the list after a selection.
// these are just timers for demonstration, any observable should be fine.
const incoming$ = timer(1000, 1000).pipe(map(x => String.fromCharCode(x + 65)), take(10));
const selected$ = timer(3000, 3000).pipe(map(x => String.fromCharCode(x * 2 + 66)), take(2));
merge(
incoming$.pipe(map(x => (s) => [...s, x])), // append to list
selected$.pipe(map(x => (s) => { // slice list starting from selection
const index = s.indexOf(x);
return (index !== -1) ? s.slice(index) : s;
}))
).pipe(
scan((list, reducer) => reducer(list), []) // run reducer
).subscribe(x => console.log(x)); // display list state as demonstration.
If I understand the problem right, you could follow the following approach.
The key point is to recognize that the list Observable (i.e. the Observable obtained with the use of scan) should be an hot Observable, i.e. an Observable that notifies independent on whether or not it is subscribed. The reason is that each new stream you want to create should have always the same source Observable as its upstream.
Then, as you already hint, the act of selecting a value should be modeled with a BehaviorSubject.
As soon as the select BehaviorSubject notifies a value selected, the previous stream has to complete and a new one has to be subscribed. This is the job of switchMap.
The rest is to slice the arrays of numbers in the right way.
This is the complete code of this approach
const selectedObject$ = new BehaviorSubject(1);
const incomingObjects$ = interval(1000).pipe(take(10));
const incomingObjectsHot$ = new ReplaySubject<number[]>(1);
incomingObjects$
.pipe(
scan((acc, val) => {
acc.push(val);
return acc;
}, [])
)
.subscribe(incomingObjectsHot$);
selectedObject$
.pipe(
switchMap((selected) =>
incomingObjectsHot$.pipe(
map((nums) => {
const selIndex = nums.indexOf(selected);
if (selIndex > 0) {
return nums.slice(selIndex);
}
})
)
),
filter(v => !!v)
)
.subscribe(console.log);
An example can be seen in this stackblitz.

Why does operator in piped Subject is called for every subscription?

I need to filter values from Subject and do some side-effects on returned data.
Something like this:
const subject2 = subject.pipe(
filter((value: number) => {
console.log(`filter: ${value}`);
return value % 2 === 0; // filter even nubmers
}),
tap((value) => console.log(`after filter: ${value}`))
);
I see that function from filter() is called for every value emitted to subject2 subscribers (i.e. as many times as subject2 subscribers length). But I assumed that it will be called once for every next() call.
Also I see that if I subscribe to subject2 and pipe its values, no duplication appears.
Could someone please explain what's going on behind the scene and what is the correct pattern of filtering subject values?
Example on Stackblitz:
https://stackblitz.com/edit/typescript-e4stc4?devtoolsheight=100&file=index.ts
Behind the scenes, the Subject's next method is implemented like this:
for (const observer of this.observers) {
observer.next(value);
}
So each "observer" (or "subscriber") will get its own notification when you emit into the Subject. The operators are just functions that process the value before the result is passed to the observer.
For example if you declared the operators like this:
const myFilter = filter((value: number) => value % 2 === 0);
const myTap = tap((value) => console.log(`after filter: ${value}`));
Then the next function inside a custom Subject could be implemented like this:
for (const observer of this.observers) {
observer.next(myTap(myFilter(value)));
}
(This code wouldn't actually work - it's a simplification to show how the values reach the subscriber when you call next on a Subject)
To solve your issue, you can reduce the number of observers to the source Subject by putting a share() as the last element of the chain like so:
const subject2 = subject.pipe(
filter((value: number) => {
console.log(`filter: ${value}`);
return value % 2 === 0; // filter even nubmers
}),
tap((value) => console.log(`after filter: ${value}`)),
share()
);
share is implemented such that it acts as a single observer to the source Observable no matter how many observers are subscribed to it.

What is the difference between tap and map in RxJS?

I read the difference from the article but the main points look like this.
so with tap I can change the variables such as that if I put x=3+4 then it changes the values of variable then I can say there is one side effect.
But with map I can change the value looping each value, isn't it?
Can you pinpoint what outstanding differences they have?
tap
RxJS tap performs side effects for every value emitted by source Observable and returns an Observable identical to the source Observable until there is no error.
map
map is a RxJS pipeable operator. map applies a given function to each element emitted by the source Observable and emits the resulting values as an Observable
A mapping function takes a thing and returns another thing. e.g. I can build a function that takes 10 and returns 11, that takes 11 and returns 12, etc.
const inc = n => n + 1;
Array#map applies such mapping function to all elements of an array but "map" doesn't mean "iteration".
In RxJS, when a data is sent to the stream it goes through a series of operators:
The map operator will simply apply a function to that data and return the result.
The tap operator however takes a data, apply a function to that data but returns the original data, if the function bothered to return a result, tap just ignores it.
Here's an example:
We push 10 to stream a$, tap just log the value. We know that console.log always return undefined but that's fine because tap simply returns its parameter.
We push 10 to stream b$, it goes through map(inc) which applies inc to 10 returning 11.
const a$ = of(10).pipe(tap(n => console.log(`tap: ${n}`)));
const b$ = of(10).pipe(map(inc));
a$.subscribe(n => console.log(`n from a$: ${n}`));
b$.subscribe(n => console.log(`n from b$: ${n}`));
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.5/rxjs.umd.min.js"></script>
<script>
const {of} = rxjs;
const {map, tap} = rxjs.operators;
const inc = n => n + 1;
</script>
Tap should be Used for Notification, logging non-contextual/critical side effects.
It's like a "peek" into the "pipe". The data stays the same, You can do something with it. Some data goes in, you look, same data comes out.
Map is for transformation/mapping of the Data in the "pipe". Some data comes in, different/transformed data comes out.
The purpose of tap is to execute an action keeping the same value
of the observable
The purpose of map is to transform the emitted values of the
observable
const messagesCount$ = newMessages$
.pipe(tap(messages => notificationService.notify('You have ' + message.length + ' message(s)')))
.pipe(map(messages => messages.length))
The tap and map are both RxJS operators, RxJS operators are just function that performs some manipulation over the data.
Both of them are pipeable operators which takes input as Observable, perform some action and return an output observable.
Difference between map and tap:
The map is a pipeable operator that takes an input observable, performs some manipulation on it and returns a new manipulated observable. For example
const source$ = of(1,2,3) // observable which will emit 1,2,3
// It take an input observable and return a new observable which will emit square of input values.
// So, the output observable will emit 1,4,9
const mapSource$ = of(1,2,3)
.pipe(map(value => value * value))
The tap operator on another hand takes an input observable perform some action and returns the same input observable.
const source$ = of(1,2,3) // observable which will emit 1,2,3
// It take an input observable and return a same observable after console value.
// So, the output observable will emit 1,2,3
const tapSource$ = of(1,2,3)
.pipe(tap(value => console.log(value)))
you can think of tap operator as a void function that whatever it does to the input value it does not change the original value
const source = of(1, 2, 3, 4, 5);
// here we are manipulating the input value but the output value of the observable still the same
const example = source.pipe(
tap(val => val + 100),
);
// output: 1, 2, 3, 4, 5
const subscribe = example.subscribe(val => console.log(val));
in the other hand if we made any manipulation of the input values of the observable using the map operator it will change the output values
const example = source.pipe(
map(val => val + 100)
);
// output: 101, 102, 103, 104, 105
const subscribe = example.subscribe(val => console.log(val));
I addition to what the others are saying, in Rxjs 7.4 tap now has three more subscribe handlers, so you can use it to get notified on subscribe, unsubscribe and finalize:
https://github.com/ReactiveX/rxjs/commit/eb26cbc4488c9953cdde565b598b1dbdeeeee9ea#diff-93cd3ac7329d72ed4ded62c6cbae17b6bdceb643fa7c1faa6f389729773364cc
This is great for debugging purposes, so you can use tap to find out much more about what is happening with your stream.
Example:
const subscription = subject
.pipe(
tap({
subscribe: () => console.log('subscribe'),
next: (value) => console.log(`next ${value}`),
error: (err) => console.log(`error: ${err.message}`),
complete: () => console.log('complete'),
unsubscribe: () => console.log('unsubscribe'),
finalize: () => console.log('finalize'),
})
)
.subscribe();
TAP, can NOT transform:
interval(1000).pipe(tap(el=> el*2)).subscribe(console.log); // 0,1,2,3
MAP, CAN transform:
interval(1000).pipe(map(el=> el*2)).subscribe(console.log); // 0,2,4,6
If you do NOT need to transform the value, just console.log it or run external function to pass the Original value = TAP is good.
If you NEED TO TRANSFORM/CHANGE the value = MAP is the way to go.

Conditionally producing multiple values based on item value and merging it into the original stream

I have a scenario where I need to make a request to an endpoint, and then based on the return I need to either produce multiple items or just pass an item through (specifically I am using redux-observable and trying to produce multiple actions based on an api return if it matters).
I have a simplified example below but it doesn't feel like idiomatic rx and just feels weird. In the example if the value is even I want to produce two items, but if odd, just pass the value through. What is the "right" way to achieve this?
test('url and response can be flatMap-ed into multiple objects based on array response and their values', async () => {
const fakeUrl = 'url';
axios.request.mockImplementationOnce(() => Promise.resolve({ data: [0, 1, 2] }));
const operation$ = of(fakeUrl).pipe(
mergeMap(url => request(url)),
mergeMap(resp => resp.data),
mergeMap(i =>
merge(
of(i).pipe(map(num => `number was ${num}`)),
of(i).pipe(
filter(num => num % 2 === 0),
map(() => `number was even`)
)
)
)
);
const result = await operation$.pipe(toArray()).toPromise();
expect(result).toHaveLength(5);
expect(axios.request).toHaveBeenCalledTimes(1);
});
Personally I'd do it in a very similar way. You just don't need to be using the inner merge for both cases:
...
mergeMap(i => {
const source = of(`number was ${i}`);
return i % 2 === 0 ? merge(source, of(`number was even`)) : source;
})
I'm using concat to append a value after source Observable completes. Btw, in future RxJS versions there'll be endWith operator that will make it more obvious. https://github.com/ReactiveX/rxjs/pull/3679
Try to use such combo - partition + merge.
Here is an example (just a scratch)
const target$ = Observable.of('single value');
const [streamOne$, streamTwo$] = target$.partition((v) => v === 'single value');
// some actions with your streams - mapping/filtering etc.
const result$ = Observable.merge(streamOne$, streamTwo$)';

Have withLatestFrom wait until all sources have produced one value

I'm making use of the withLatestFrom operator in RxJS in the normal way:
var combined = source1.withLatestFrom(source2, source3);
...to actively collect the most recent emission from source2 and source3 and to emit all three value only when source1 emits.
But I cannot guarantee that source2 or source3 will have produced values before source1 produces a value. Instead I need to wait until all three sources produce at least one value each before letting withLatestFrom do its thing.
The contract needs to be: if source1 emits then combined will always eventually emit when the other sources finally produce. If source1 emits multiple times while waiting for the other sources we can use the latest value and discard the previous values. Edit: as a marble diagram:
--1------------2---- (source)
----a-----b--------- (other1)
------x-----y------- (other2)
------1ax------2by--
--1------------2---- (source)
------a---b--------- (other1)
--x---------y------- (other2)
------1ax------2by--
------1--------2---- (source)
----a-----b--------- (other1)
--x---------y------- (other2)
------1ax------2by--
I can make a custom operator for this, but I want to make sure I'm not missing an obvious way to do this using the vanilla operators. It feels almost like I want combineLatest for the initial emit and then to switch to withLatestFrom from then on but I haven't been able to figure out how to do that.
Edit: Full code example from final solution:
var Dispatcher = new Rx.Subject();
var source1 = Dispatcher.filter(x => x === 'foo');
var source2 = Dispatcher.filter(x => x === 'bar');
var source3 = Dispatcher.filter(x => x === 'baz');
var combined = source1.publish(function(s1) {
return source2.publish(function(s2) {
return source3.publish(function(s3) {
var cL = s1.combineLatest(s2, s3).take(1).do(() => console.log('cL'));
var wLF = s1.skip(1).withLatestFrom(s2, s3).do(() => console.log('wLF'));
return Rx.Observable.merge(cL, wLF);
});
});
});
var sub1 = combined.subscribe(x => console.log('x', x));
// These can arrive in any order
// and we can get multiple values from any one.
Dispatcher.onNext('foo');
Dispatcher.onNext('bar');
Dispatcher.onNext('foo');
Dispatcher.onNext('baz');
// combineLatest triggers once we have all values.
// cL
// x ["foo", "bar", "baz"]
// withLatestFrom takes over from there.
Dispatcher.onNext('foo');
Dispatcher.onNext('bar');
Dispatcher.onNext('foo');
// wLF
// x ["foo", "bar", "baz"]
// wLF
// x ["foo", "bar", "baz"]
I think the answer is more or less as you described, let the first value be a combineLatest, then switch to withLatestFrom. My JS is hazy, but I think it would look something like this:
var selector = function(x,y,z) {};
var combined = Rx.Observable.concat(
source1.combineLatest(source2, source3, selector).take(1),
source1.withLatestFrom(source2, source3, selector)
);
You should probably use publish to avoid multiple subscriptions, so that would look like this:
var combined = source1.publish(function(s1)
{
return source2.publish(function(s2)
{
return source3.publish(function(s3)
{
return Rx.Observable.concat(
s1.combineLatest(s2, s3, selector).take(1),
s1.withLatestFrom(s2, s3, selector)
);
});
});
});
or using arrow functions...
var combined = source1.publish(s1 => source2.publish(s2 => source3.publish(s3 =>
Rx.Observable.concat(
s1.combineLatest(s2, s3, selector).take(1),
s1.withLatestFrom(s2, s3, selector)
)
)));
EDIT:
I see the problem with concat, the withLatestFrom isn't getting the values. I think the following would work:
var combined = source1.publish(s1 => source2.publish(s2 => source3.publish(s3 =>
Rx.Observable.merge(
s1.combineLatest(s2, s3, selector).take(1),
s1.skip(1).withLatestFrom(s2, s3, selector)
)
)));
...so take one value using combineLatest, then get the rest using withLatestFrom.
I wasn't quite satisfied with the accepted answer, so I ended up finding another solution. Many ways to skin a cat!
My use-case involves just two streams - a "requests" stream and a "tokens" stream. I want requests to fire as soon as they are received, using the whatever the latest token is. If there is no token yet, then it should wait until the first token appears, and then fire off all the pending requests.
I wasn't quite satisfied with the accepted answer, so I ended up finding another solution. Essentially I split the request stream into two parts - before and after first token arrives. I buffer the first part, and then re-release everything in one go once I know that the token stream is non-empty.
const first = token$.first()
Rx.Observable.merge(
request$.buffer(first).mergeAll(),
request$.skipUntil(first)
)
.withLatestFrom(token$)
See it live here: https://rxviz.com/v/VOK2GEoX
For RxJs 7:
const first = token$.first()
merge(
request$.pipe(
buffer(first),
mergeAll()
),
request$.pipe(
skipUntil(first)
)
).pipe(
withLatestFrom(token$)
)
I had similar requirements but for just 2 observables.
I ended up using switchMap+first:
observable1
.switchMap(() => observable2.first(), (a, b) => [a, b])
.subscribe(([a, b]) => {...}));
So it:
waits until both observables emit some value
pulls the value from second observable only if the first one has changed (unlike combineLatest)
doesn't hang subscribed on second observable (because of .first())
In my case, second observable is a ReplaySubject. I'm not sure if it will work with other observable types.
I think that:
flatMap would probably work too
it might be possible to extend this approach to handle more than 2 observables
I was surprised that withLatestFrom will not wait on second observable.
In my mind, the most elegant way to achieve the different behavior of an existing RxJS operator is to wrap it into a custom operator. So that from the outside it looks just like any regular operator and doesn't require you to restructure your code each time you need this behavior.
Here is how you can create your own operator which behaves just like withLatestFrom, except that at the very beginning it will emit as soon as the first value of the target observable is emitted (unlike standard withLatestFrom, which will ignore the first emission of the source if the target hasn't yet emitted once). Let's call it delayedWithLatestFrom.
Note that it's written in TypeScript, but you can easily transform it to plain JS. Also, it's a simple version that supports only one target observable and no selector function - you can extend it as needed from here.
export function delayedWithLatestFrom<T, N>(
target$: Observable<N>
): OperatorFunction<T, [T, N]> {
// special value to avoid accidental match with values that could originate from target$
const uniqueSymbol = Symbol('withLatestFromIgnore');
return pipe(
// emit as soon target observable emits the first value
combineLatestWith<T, [N]>(target$.pipe(first())),
// skip the first emission because it's handled above, and then continue like a normal `withLatestFrom` operator
withLatestFrom(target$.pipe(skip(1), startWith(uniqueSymbol))),
map(([[rest, combineLatestValue], withLatestValue]) => {
// take combineLatestValue for the first time, and then always take withLatestValue
const appendedValue =
withLatestValue === uniqueSymbol ? combineLatestValue : withLatestValue;
return [rest, appendedValue];
})
);
}
// SAMPLE USAGE
source$.pipe(
delayedWithLatestFrom(target$)
).subscribe(console.log);
So if you compare it with the original marble diagram for withLatestFrom, it will differ only in one fact: while withLatestFrom ignores the first emissions and produces b1 as the first value, the delayedWithlatestFrom operator will emit one more value a1 at the beginning, as soon as the second observable emits 1.
a) Standard withLatestFrom:
b) Custom delayedWithLatestFrom:
Use combineLatest and filter to remove tuples before first full set is found then set a variable to stop filtering. The variable can be within the scope of a wrapping defer to do things properly (support resubscription). Here it is in java (but the same operators exist in RxJs):
Observable.defer(
boolean emittedOne = false;
return Observable.combineLatest(s1, s2, s3, selector)
.filter(x -> {
if (emittedOne)
return true;
else {
if (hasAll(x)) {
emittedOne = true;
return true;
} else
return false;
}
});
)
I wanted a version where tokens are fetched regularly - and where I want to retry the main data post on (network) failure. I found shareReplay to be the key. The first mergeWith creates a "muted" stream, which causes the first token to be fetched immediately, not when the first action arrives. In the unlikely event that the first token will still not be available in time, the logic also has a startWith with an invalid value. This causes the retry logic to pause and try again. (Some/map is just a Maybe-monad):
Some(fetchToken$.pipe(shareReplay({refCount: false, bufferSize: 1})))
.map(fetchToken$ =>
actions$.pipe(
// This line is just for starting the loadToken loop immediately, not waiting until first write arrives.
mergeWith(fetchToken$.pipe(map(() => true), catchError(() => of(false)), tap(x => loggers.info(`New token received, success: ${x}`)), mergeMap(() => of()))),
concatMap(action =>
of(action).pipe(
withLatestFrom(fetchToken$.pipe(startWith(""))),
mergeMap(([x, token]) => (!token ? throwError(() => "Token not ready") : of([x, token] as const))),
mergeMap(([{sessionId, visitId, events, eventIds}, token]) => writer(sessionId, visitId, events, token).pipe(map(() => <ISessionEventIdPair>{sessionId, eventIds}))),
retryWhen(errors =>
errors.pipe(
tap(err => loggers.warn(`Error writing data to WG; ${err?.message || err}`)),
mergeMap((_error: any, attemptIdx) => (attemptIdx >= retryPolicy.retryCount ? throwError(() => Error("It's enough now, already")) : of(attemptIdx))), // error?.response?.status (int, response code) error.code === "ENOTFOUND" / isAxiosError: true / response === undefined
delayWhen(attempt => timer(attempt < 2 ? retryPolicy.shortRetry : retryPolicy.longRetry, scheduler))
)
)
)
),
)
)
Thanks to everyone on this question-page for good inputs.
Based on the answer from #cjol
Here's a RxJs 7 implementation of a waitFor operator that will buffer the source stream until all input observables have emitted values, then emit all buffered events on the source stream. Any subsequent events on the source stream are emitted immediately.
// Copied from the definition of withLatestFrom() operator.
export function waitFor<T, O extends unknown[]>(
inputs: [...ObservableInputTuple<O>]
): OperatorFunction<T, [T, ...O]>;
/**
* Buffers the source until every observable in "from" have emitted a value. Then
* emit all buffered source values with the latest values of the "from" array.
* Any source events are emitted immediately after that.
* #param from Array of observables to wait for.
* #returns Observable that emits an array that concatenates the source and the observables to wait.
*/
export function waitFor(
from: Observable<unknown>[]
): (source$: Observable<unknown>) => Observable<unknown> {
const combined$ = combineLatest(from);
// This served as a conditional that switched on and off the streams that
// wait for the the other observables, or emits the source right away because
// the other observables have emitted.
const firstCombined$ = combined$.pipe(first());
return function (source$: Observable<unknown>): Observable<unknown> {
return merge(
// This stream will buffer the source until the other observables have all emitted.
source$.pipe(
takeUntil(firstCombined$), // without this it continues to buffer new values forever
buffer(firstCombined$),
mergeAll()
),
// This stream emits the source straight away and will take over when the other
// observables have emitted.
source$.pipe(skipUntil(firstCombined$))
).pipe(
withLatestFrom(combined$),
// Flatten it to behave like withLatestFrom() operator.
map(([source, combined]) => [source, ...combined])
);
};
}
All of the above solutions are not really on the point, therefore I made my own. Hope it helps someone out.
import {
combineLatest,
take,
map,
ObservableInputTuple,
OperatorFunction,
pipe,
switchMap
} from 'rxjs';
/**
* ### Description
* Works similar to {#link withLatestFrom} with the main difference that it awaits the observables.
* When all observables can emit at least one value, then takes the latest state of all observables and proceeds execution of the pipe.
* Will execute this pipe only once and will only retrigger pipe execution if source observable emits a new value.
*
* ### Example
* ```ts
* import { BehaviorSubject } from 'rxjs';
* import { awaitLatestFrom } from './await-latest-from.ts';
*
* const myNumber$ = new BehaviorSubject<number>(1);
* const myString$ = new BehaviorSubject<string>("Some text.");
* const myBoolean$ = new BehaviorSubject<boolean>(true);
*
* myNumber$.pipe(
* awaitLatestFrom([myString$, myBoolean$])
* ).subscribe(([myNumber, myString, myBoolean]) => {});
* ```
* ### Additional
* #param observables - the observables of which the latest value will be taken when all of them have a value.
* #returns a tuple which contains the source value as well as the values of the observables which are passed as input.
*/
export function awaitLatestFrom<T, O extends unknown[]>(
observables: [...ObservableInputTuple<O>]
): OperatorFunction<T, [T, ...O]> {
return pipe(
switchMap((sourceValue) =>
combineLatest(observables).pipe(
take(1),
map((values) => [sourceValue, ...values] as unknown as [T, ...O])
)
)
);
}
Actually withLatestFrom already
waits for every source
emits only when source1 emits
remembers only the last source1-message while the other sources are yet to start
// when source 1 emits the others have emitted already
var source1 = Rx.Observable.interval(500).take(7)
var source2 = Rx.Observable.interval(100, 300).take(10)
var source3 = Rx.Observable.interval(200).take(10)
var selector = (a,b,c) => [a,b,c]
source1
.withLatestFrom(source2, source3, selector)
.subscribe()
vs
// source1 emits first, withLatestFrom discards 1 value from source1
var source1 = Rx.Observable.interval(500).take(7)
var source2 = Rx.Observable.interval(1000, 300).take(10)
var source3 = Rx.Observable.interval(2000).take(10)
var selector = (a,b,c) => [a,b,c]
source1
.withLatestFrom(source2, source3, selector)
.subscribe()

Resources