Ветвление и (пере) объединение пар значений в RxJS

1

Я хочу создать поток, который

  1. разделяет значения и процессы, каждая из которых частично разделяется на отдельный поток
  2. каждый поток будет преобразовывать данные, я не могу контролировать применяемое преобразование
  3. (re-) соединяет частичные значения с их соответствующей встречной частью

Причина, по которой я хочу это сделать, - обеспечить целостность значения. Или, по крайней мере, для некоторой его части.

Поскольку каждый поток может иметь несколько асинхронных операций, они не отображаются в порядке, когда соединяются потоки. Использование некоторого типа concat() также не работает, поскольку оно блокирует все входящие значения. Обработка должна выполняться параллельно.

Проиллюстрировать:

                            o
                            |
                            | [{a1,b1}, {a2,b2}, ...]
                            |
                            +
                           / \
                   {a<x>} /   \ {b<x>}
                         /     \
                        |       |
                        |       + async(b<x>) -> b'<x>
                        |       |
                        \       /
                         \     /
                          \   /
                           \ /
                            + join(a<x>, b'<x>)
                            |
                            | [{a1,b'1}, {a2,b'2}, ...]
                            |
                       (subscribe)

Я знаю, что это можно сделать с помощью функции result selector. Например

input$.mergeMap(
  ({a, b}) => Rx.Observable.of(b).let(async), 
  ({a}, processedB) => ({a, b:processedB})
);

Но (а) это приведет к тому, что async всегда будет устанавливаться/сбрасываться для каждого значения. Я хотел бы, чтобы частичный поток только инициализировался один раз. Кроме того, (б) это работает только с одним асинхронным потоком.

Я также пытался использовать window*, но не мог понять, как снова присоединиться к значениям. Также пытался использовать goupBy без везения.


РЕДАКТИРОВАТЬ:

Вот моя текущая попытка. Он имеет упомянутый вопрос (а). Init... и Completed... должен регистрироваться только один раз.

const doSomethignAsync = data$ => {
  console.log('Init...') // Should happen once.
  return data$
    .mergeMap(val => Rx.Observable.of(val.data).delay(val.delay))
    .finally(() => console.log('Completed...')); // Should never happen
};

const input$ = new Rx.Subject();
const out$ = input$
  .mergeMap(
    ({ a, b }) => Rx.Observable.of(b).let(doSomethignAsync),
    ({ a }, asyncResult ) => ({ a, b:asyncResult })
  )
  
  .subscribe(({a, b}) => {
    if (a === b) { 
      console.log('Re-joined [${a}, ${b}] correctly.');
    } else {
      console.log('Joined [${a}, ${b}]...'); // Should never happen
    }
  });


input$.next({ a: 1, b: { data: 1, delay: 2000 } });
input$.next({ a: 2, b: { data: 2, delay: 1000 } });
input$.next({ a: 3, b: { data: 3, delay: 3000 } });
input$.next({ a: 4, b: { data: 4, delay: 0 } });
<script src="https://unpkg.com/rxjs/bundles/Rx.min.js"></script>
Теги:
rxjs
rxjs5

1 ответ

1
Лучший ответ

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

Тем не менее, здесь один из возможных способов, который делает кучу допущений. Он был несколько общим, как пользовательский оператор, который можно использовать с let.

(примечание стороны: я назвал это "сопоставить", но это плохое и очень вводящее в заблуждение имя для этого, но не имеет времени, чтобы называть вещи...)

const collate = (...segments) => source$ =>
  source$
    .mergeMap((obj, index) => {
      return segments.map(({ key, work }) => {
        const input = obj[key];
        const output$ = work(input);

        return Observable.from(output$).map(output => ({
          index,
          result: { [key]: output }
        }))
      })
    })
    .mergeAll()
    .groupBy(
      obj => obj.index,
      obj => obj.result,
      group$ => group$.skip(segments.length - 1)
    )
    .mergeMap(group$ =>
      group$.reduce(
        (obj, result) => Object.assign(obj, result),
        {}
      )
    );

И вот пример использования:

const result$ = input$.let(
  collate({
    key: 'a',
    work: a => {
      // do stuff with "a"
      return Observable.of(a).map(d => d + '-processed-A');
    }
  }, {
    key: 'b',
    work: b => {
      // do stuff with "b"
      return Observable.of(b).map(d => d + '-processed-B');
    }
  })
);

Учитывая входные данные { a: '1', b: '1 } он выдаст { a: '1-processed-A', b: '1-processed-B' } и т.д., Сгруппированные правильно, делая столько же одновременно, сколько возможно - единственная буферизация, которую он выполняет, - это сопоставление всех сегментов для конкретного ввода.

Здесь работает демонстрационная версия https://jsbin.com/yuruvar/edit?js,output


Сломать

Там, вероятно, более понятные/более простые способы сделать это, особенно если вы можете жестко кодировать некоторые вещи, а не создавать их общие. Но пусть разбивает то, что я сделал.

const collate = (...segments) => source$ =>
  source$
    // for every input obj we use the index as an ID
    // (which is provided by Rx as autoincrementing)
    .mergeMap((obj, index) => {
      // segments is the configuration of how we should
      // chunk our data into concurrent processing channels.
      // So we're returning an array, which mergeMap will consume
      // as if it were an Observable, or we could have used
      // Observable.from(arr) to be even more clear
      return segments.map(({ key, work }) => {
        const input = obj[key];
        // the 'work' function is expected to return
        // something Observable-like
        const output$ = work(input);

        return Observable.from(output$).map(output => ({
          // Placing the index we closed over lets us later
          // stitch each segment back to together
          index,
          result: { [key]: output }
        }))
      })
    })
    // I had returned Array<Observable> in mergeMap
    // so we need to flatten one more level. This is
    // rather confusing...prolly clearer ways but #YOLO
    .mergeAll()
    // now we have a stream of all results for each segment
    // in no guaranteed order so we need to group them together
    .groupBy(
      obj => obj.index,
      obj => obj.result,
      // this is tough to explain. this is used as a notifier
      // to say when to complete() the group$, we want complete() it
      // after we've received every segment for that group, so in the
      // notifier we skip all except the last one we expect
      // but remember this doesn't skip the elements downstream!
      // only as part of the durationSelector notifier
      group$ => group$.skip(segments.length - 1)
    )
    .mergeMap(group$ =>
      // merge every segment object that comes back into one object
      // so it has the same shape as it came in, but which the results
      group$.reduce(
        (obj, result) => Object.assign(obj, result),
        {}
      )
    );

Я не думал и не беспокоился о том, как обработка ошибок/распространение может работать, потому что это сильно зависит от вашего usecase. Если у вас нет контроля над обработкой каждого сегмента, то также можно .take(1) какой-то тайм-аут и .take(1), иначе вы можете пропустить подписки.

  • 0
    Спасибо тебе большое за это. Я все еще ищу решение, чтобы понять все. Он имеет несколько хороших шаблонов в работе с Observable s, которые я никогда не мог придумать! :-)

Ещё вопросы

Сообщество Overcoder
Наверх
Меню