У вас есть сLatestFrom до тех пор, пока все источники не произведут одно значение

Я использую оператор withLatestFrom в RxJS обычным способом:

var combined = source1.withLatestFrom(source2, source3);

... активно собирать самую последнюю эмиссию от source2 и source3 и излучать все три значения только тогда, когда испускает source1.

Но я не могу гарантировать, что source2 или source3 будут выдавать значения до того, как source1 выдает значение. Вместо этого мне нужно подождать, пока все три источника не произведут хотя бы одно значение, прежде чем позволить withLatestFrom выполнить свою задачу.

Контракт должен быть: если source1 испускает, то combined будет всегда в конечном итоге испускаться, когда в конечном итоге будут получены другие источники. Если source1 излучает несколько раз, ожидая других источников, мы можем использовать последнее значение и отменить предыдущие значения. Изменить: как мраморную диаграмму:

--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--

Я могу создать для этого пользовательский оператор, но я хочу убедиться, что я не пропустил очевидный способ сделать это с помощью ванильных операторов. Это похоже на то, что я хочу combineLatest для исходного испускания, а затем перейти к withLatestFrom с этого момента, но я не смог понять, как это сделать.

Изменить: Полный пример кода из окончательного решения:

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"]

Ответ 1

Я думаю, что ответ более или менее, как вы описали, пусть первое значение будет combineLatest, а затем переключиться на withLatestFrom. Мой JS туманный, но я думаю, что это будет выглядеть примерно так:

var selector = function(x,y,z) {};

var combined = Rx.Observable.concat(
    source1.combineLatest(source2, source3, selector).take(1),
    source1.withLatestFrom(source2, source3, selector)
);

Вы, вероятно, должны использовать publish, чтобы избежать нескольких подписок, чтобы это выглядело так:

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)
            );
        });
    });
});

или используя функции стрелок...

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:

Я вижу проблему с concat, withLatestFrom не получает значения. Я думаю, что будет работать следующее:

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

... поэтому возьмите одно значение с помощью combineLatest, а затем получите остальное с помощью withLatestFrom.

Ответ 2

Я не был полностью удовлетворен принятым ответом, поэтому я нашел другое решение. Множество способов снять шкуру с кошки!

Мой вариант использования включает в себя только два потока - поток "запросов" и поток "токенов". Я хочу, чтобы запросы запускались сразу после их получения, используя любой последний токен. Если токена еще нет, он должен дождаться появления первого токена, а затем запустить все ожидающие запросы.

Я не был полностью удовлетворен принятым ответом, поэтому я нашел другое решение. По сути, я разделил поток запросов на две части - до и после получения первого токена. Я буферизирую первую часть, а затем перезагружаю все за один раз, когда узнаю, что поток токенов не пуст.

const first = token$.first()

Rx.Observable.merge(
  request$.buffer(first).mergeAll(),
  request$.skipUntil(first)
)
  .withLatestFrom(token$)

Смотрите это в прямом эфире здесь: https://rxviz.com/v/VOK2GEoX

Ответ 3

Используйте combineLatest и filter для удаления кортежей до того, как будет найден первый полный набор, затем установите переменную, чтобы остановить фильтрацию. Переменная может находиться в пределах области обертывания defer для правильной работы (поддержка повторной подписки). Здесь он находится в java (но те же самые операторы существуют в 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; 
            }
        });
)

Ответ 4

У меня были аналогичные требования, но только для двух наблюдаемых. Я сначала использовал switchMap +:

observable1
 .switchMap(() => observable2.first(), (a, b) => [a, b])
 .subscribe(([a, b]) => {...}));

Итак:

  • ожидает, пока оба наблюдаемых не выдадут какое-то значение
  • извлекает значение из второго наблюдаемого только в том случае, если первый изменился (в отличие от combLatest)
  • не зависает на втором наблюдаемом (из-за .first())

В моем случае вторым наблюдаемым является ReplaySubject. Я не уверен, что он будет работать с другими наблюдаемыми типами.

Я думаю, что:

  • flatMap, вероятно, тоже будет работать.
  • можно было бы расширить этот подход для обработки более двух наблюдаемых

Я был удивлен, что withLatestFrom не будет ждать второго наблюдаемого.

Ответ 5

На самом деле withLatestFrom уже

  • ждет каждого источника
  • испускается только тогда, когда source1 испускает
  • запоминает только последнее сообщение источника1, пока другие источники еще не запущены.
// 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()

против

// 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()