Padding zip() with undefined - rxjs

The following
Rx.Observable.zip(
Rx.Observable.of(1,2),
Rx.Observable.of("a"))
.subscribe(p => console.log(p))
produces
1,a
which makes sense, but what I want it to produce is
1,a
2,undefined
I want to pad the shorter observable with undefineds until the longer one completes. Any suggestions?

I realise adding delay can turn the .of operator async and with scan you can replace the same value with undefine
Rx.Observable.combineLatest(
Rx.Observable.of(1,2).delay(0),
Rx.Observable.of("a"))
.scan((acc,curr)=>{
acc[1]=acc[1]==curr[1]?undefined:curr[1]
acc[0]=acc[0]==curr[0]?undefined:curr[0]
return acc
},[])
.subscribe(p => console.log(p))

I think the key to this is to ensure that all of the source observables are the same length.
One solution would be to compose a counter observable that's as long as the longest source observable. It could then be concatenated to the shorter source observables, like this:
const pad = (...sources) => Rx.Observable.create(observer => {
// Publish the source observables to make them multicast
// and to allow the subscription order to be managed.
const publishedSources = sources.map(source => source.publish());
// Create an observable that emits an incremented index and
// is as long as the longest source observable.
const counter = Rx.Observable
.merge(...publishedSources.map(
source => source.map((unused, index) => index)
))
.scan((max, index) => Math.max(max, index), 0)
.distinctUntilChanged()
.publish();
// Zip the published sources, contatenating the counter so
// that they are all the same length. When the counter
// emissions are concatenated, they are mapped to undefined.
const subscription = Rx.Observable.zip(...publishedSources.map(
source => source.concat(counter.mapTo(undefined))
)).subscribe(observer);
// Connect the counter and the published sources.
subscription.add(counter.connect());
publishedSources.forEach(
source => subscription.add(source.connect())
);
return subscription;
});
pad(
Rx.Observable.of(1, 2),
Rx.Observable.of("a")
).subscribe(padded => console.log(padded));
.as-console-wrapper { max-height: 100% !important; top: 0; }
<script src="https://unpkg.com/rxjs#5/bundles/Rx.min.js"></script>

My own solution is below. The idea is that all the observables are converted into endless streams, starting with the original values, enclosed in objects, and then an infinite number of undefineds. These endless streams are concatted and the result is taken as long as any of the values are original.
const unending = (obs) =>
obs.map(value => ({value}))
.concat(Rx.Observable
.interval(0)
.mapTo(undefined));
const zipPad = (...obss) =>
Rx.Observable.zip(...obss.map(unending))
.takeWhile(p => p.find(v => v))
.map(p => p.map(v => v && v.value));
zipPad(Rx.Observable.of(1,2,3),
Rx.Observable.of("a"),
Rx.Observable.of("x", "y"))
.subscribe(p => console.log(p));
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.6/Rx.min.js"></script>
Anyone is welcome to improve on this answer and post their variations here.

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.

RxJS: match two observables using custom logic

I have two observable streams, each emitting a series of items over an infinite period of time (similar to how a DOM-based click Observable would behave). I know an item from Observable A (a$) will match an item from Observable B (b$), but need to do some custom logic to determine which items match.
I tried to make this work, but I could only get the first pair to match, and then subsequent items never emit again...
This is an extract from the code:
a$.pipe(
mergeMap(a => {
return b$.pipe(
filter(b => b.key.includes(a.subKey)), // custom matching logic goes here
take(1),
map(b => ({ a, b }))
);
})
)
.subscribe(({ a, b }) => {
console.log("do something with a and b", a, b);
});
Note that both Observables never complete, so if some item a from a$ emitted, its "pair" might not have been emitted from b yet. That's why I used filter and not find above. When I did find a matching item, I can complete the inner observable, since that pair has been matched & handled.
Please advise, what am I missing?
Have you looked at combine latest? It emits the latest value for both streams once both have emitted once.
combineLatest(a$, b$).pipe(filter(([a, b]) => b.key.includes(a.subKey)))
.subscribe(([a, b]) => {
// Do stuff with an and b here
});
I think one way to solve this is to first accumulate the values of each observable in a map, then to use the combineLatest operator so you can check for pairs on every emission.
const aMap$ = a$.pipe(scan((acc, crt) => (acc[crt.id] = crt, acc), Object.create(null)));
const bMap$ = b$.pipe(scan((acc, crt) => (acc[crt.id] = crt, acc), Object.create(null)));
combineLatest(aMap$, bMap$)
.pipe(
map(([aMap, bMap]) => {
let pair = null;
for (const bKey in bMap) {
const bVal = bMap[bKey];
const aPairKey = bVal.keys.find(k => !!aMap[k]);
if (aPairKey) {
pair = { a: aMap[aPairKey], b: bVal };
delete aMap[aPairKey];
delete bMap[bKey];
break;
}
}
return pair;
}),
filter(v => !!v)
)
I would accumulate the values from A and B to see which values happened yet. After you can create an intersection from those arrays
const keysOccuredInA$ = a$.pipe(
map(a => a.subKey),
scan((acc, curr) => ([...acc, curr]), []),
);
const keysOccuredInB$ = b$.pipe(
map(b => b.key), // b.key is an array, right?
scan((acc, curr) => ([...acc, ...curr]), []),
);
keysOccuredInBoth$ = combineLatest(keysOccuredInA$, keysOccuredInB$).pipe(
map(([keysOccuredInA, keysOccuredInB]) => _intersection(keysOccuredInA, keysOccuredInB)), // lodash intersection
)

In rxjs how can I create groups of sequential values?

I have a stream of values in this form:
[1,2,3,1,2,1,1,1,2...]
I would like to convert it to a stream of groups in the form:
[[1,2,3],[1,2],[1],[1],[1,2]...]
New group should be created every time value becomes 1.
You can use the bufferWhen() operator to collect emitted values until an observable emits a value. In this example I use a subject to emit a shallow copy of the stream to the buffer.
The buffer will emit when ever the number 1 is emitted. If the stream starts with 1 then an empty array is emitted. So I filter that away.
const {from, Subject} = rxjs;
const {filter, bufferWhen, tap, skip} = rxjs.operators;
const stream = from([1,2,3,1,2,1,1,1,2]);
const trigger = new Subject();
stream.pipe(
tap(v => trigger.next(v)),
bufferWhen(() => trigger.pipe(filter(v => v === 1))),
filter(v => v.length)
).subscribe(x => console.log(x));
<script src="https://unpkg.com/#reactivex/rxjs#6.x/dist/global/rxjs.umd.js"></script>
You can use the scan() if you wanted to emit the trigger when the value decreases from the previous. Which is maybe a little better in logic, but using 1 as the trigger fits with the question.
For the record, I'm posting a solution alternative to the accepted answer.
NOTE: For finite streams concat(of(1)) is required before scan(), for distinctUntilChanged to emit the last observable.
const {of, from, ReplaySubject} = rxjs;
const {map, concat, scan, distinctUntilChanged, concatAll, toArray, delay} = rxjs.operators;
//considering infinite stream
const stream = from([1,2,3,1,2,1,1,1,2]);
stream.pipe(
scan( (acc, val) => {
if(val===1){
acc.complete()
acc=new ReplaySubject();
}
acc.next(val);
return acc;
}, new ReplaySubject()),
distinctUntilChanged(),
map(toArray()),
concatAll() );
It would be nice to collect some feedback regarding preferred solution.

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$)';

Observable.prototype.concatAll does not seem to yield expected result

With this code in mind:
const Rx = require('rxjs');
var i = 3;
const obs = Rx.Observable.interval(10)
.map(() => i++)
.map(function(val){
return Rx.Observable.create(obs => {
obs.next(val)
});
})
.take(10)
.concatAll();
obs.subscribe(function(v){
console.log(v);
});
I would have expected the logged result to be something like:
[3,4,5,6,7,8,9,10,11,12]
That is, 10 values, starting with 3.
However, all we get is just
3
Does anybody know why that would be?
concatMap will wait for the first observable to complete before subscribing to the next. You forgot to add the .complete() to your inner observable, effectively having your stream only emit the first value 3 and waiting indefinitely for the first stream to complete before concatting the next to it.
Note; for a simple value emission as per your question you can also use Rx.Observable.of() instead of Rx.Observable.create()
var i = 3;
const obs = Rx.Observable.interval(10)
.map(() => i++)
.map(val => Rx.Observable.of(val))
.take(10)
.concatAll();
obs.subscribe(v => console.log(v));
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.0.3/Rx.js"></script>

Resources