I'm looking to combine streams (observables) that start and end asynchronously:
-1----1----1----1---|->
-2----2--|->
[ optional_zip(sum) ]
-1----3----3----1---|->
What I need it for: Adding audio streams together. They're streams of audio "chunks", but I'm going to represent them with integers here. So there's the first clip playing:
-1----1----1----1---|->
and then a second one starts, a bit later:
-2----2--|->
The result of combining them by sum should be:
-1----3----3----1---|->
But the standard zip completes if any of the zipped streams end. I want this optional_zip to keep going even if one of the streams ends. Is there any way of doing this in Rx, or do I have to implement it myself by modifying the existing Zip?
note: I'm using RxPy, but the community here seems small and Rx operators seem to be pretty universal across languages, so I tagged it as rx-java and rx-js too.
I'd tackle this problem by breaking it into two parts. First, I'd want something that takes an Observable<Observable<T>>
and produces an Observable<Observable<T>[]>
where the array contains only the "active" (i.e. non-complete) observables. Any time a new element is added to the outer observable, and any time one of the inner observables completes, a new array would be emitted containing the appropriate observables. This is essentially a "scan" reduction of the primary stream.
Once you've got something that can do that, you can use flatMapLatest and zip to get what you want.
My basic attempt at the first part is as follows:
function active(ss$) {
const activeStreams = new Rx.Subject();
const elements = [];
const subscriptions = [];
ss$.subscribe(s => {
var include = true;
const subscription = s.subscribe(x => {}, x => {}, x => {
include = false;
const i = elements.indexOf(s);
if (i > -1) {
elements.splice(i, 1);
activeStreams.onNext(elements.slice());
}
});
if (include) {
elements.push(s);
subscriptions.push(subscription);
activeStreams.onNext(elements.slice());
}
});
return Rx.Observable.using(
() => new Rx.Disposable(() => subscriptions.forEach(x => x.dispose())),
() => activeStreams
);
}
From there, you'd just zip it and flatten it out like so:
const zipped = active(c$).flatMapLatest(x =>
x.length === 0 ? Rx.Observable.never()
: x.length === 1 ? x[0]
: Rx.Observable.zip(x, (...args) => args.reduce((a, c) => a + c))
);
I've made the assumptions that zero active streams should yield nothing, one active stream should yield its own elements, and two or more streams should all zip together (all of which is reflected in the map application).
My (admittedly fairly limited) testing has this combination yielding the results you were after.
Great question, by the way. I've not seen anything that solves the first part of the problem (though I'm by no means an Rx expert; if someone knows of something that already does this, please post details).
So I got some code working that I think does most of what you need. Basically, I created a function zipAndContinue
that will operate like zip
, except it will continue emitting items as long as some of the underlying streams still have data to emit. This function has only been [briefly] tested with cold observables.
Also, corrections/enhancements/edits are welcome.
function zipAndContinue() {
// Augment each observable so it ends with null
const observables = Array.prototype.slice.call(arguments, 0).map(x => endWithNull(x));
const combined$ = Rx.Observable.combineLatest(observables);
// The first item from the combined stream is our first 'zipped' item
const first$ = combined$.first();
// We calculate subsequent 'zipped' item by only grabbing
// the items from the buffer that have all of the required updated
// items (remember, combineLatest emits each time any of the streams
// updates).
const subsequent$ = combined$
.skip(1)
.bufferWithCount(arguments.length)
.flatMap(zipped)
.filter(xs => !xs.every(x => x === null));
// We return the concatenation of these two streams
return first$.concat(subsequent$)
}
And here are the utility functions used:
function endWithNull(observable) {
return Rx.Observable.create(observer => {
return observable.subscribe({
onNext: x => observer.onNext(x),
onError: x => observer.onError(x),
onCompleted: () => {
observer.onNext(null);
observer.onCompleted();
}
})
})
}
function zipped(xs) {
const nonNullCounts = xs.map(xs => xs.filter(x => x !== null).length);
// The number of streams that are still emitting
const stillEmitting = Math.max.apply(null, nonNullCounts);
if (stillEmitting === 0) {
return Rx.Observable.empty();
}
// Skip any intermittent results
return Rx.Observable.from(xs).skip(stillEmitting - 1);
}
And here's sample usage:
const one$ = Rx.Observable.from([1, 2, 3, 4, 5, 6]);
const two$ = Rx.Observable.from(['one']);
const three$ = Rx.Observable.from(['a', 'b']);
zipAndContinue(one$, two$, three$)
.subscribe(x => console.log(x));
// >> [ 1, 'one', 'a' ]
// >> [ 2, null, 'b' ]
// >> [ 3, null, null ]
// >> [ 4, null, null ]
// >> [ 5, null, null ]
// >> [ 6, null, null ]
And here's a js-fiddle (you can click Run and then open the console): https://jsfiddle.net/ptx4g6wd/
来源:https://stackoverflow.com/questions/35817174/rx-a-zip-like-operator-that-continues-after-one-of-the-streams-ended