具有原始顺序的RxJS mergeMap()

时间:2019-07-15 19:13:04

标签: javascript rxjs rxjs6 rxjs-pipeable-operators

抽象问题

有什么方法可以按照外部可观察对象的原始顺序使用mergeMap的结果,同时仍然允许内部可观察对象并行运行?


更详细的解释

让我们看一下两个合并映射运算符:

  • mergeMap

    ...需要一个映射回调,以及可以同时运行多少个内部可观察对象:

    of(1, 2, 3, 4, 5, 6).pipe(
        mergeMap(number => api.get('/double', { number }), 3)
    );
    

    查看此处的操作https://codepen.io/anon/pen/qzgGdJ?editors=1010

    这将分别触发3个对123的并行请求。这些请求之一完成后,它将触发另一个对4的请求。依此类推,始终保持3个并发请求,直到处理完所有值为止。

    但是,由于先前的请求可能在后续的请求之前完成,因此生成的值可能不正确。所以代替:

    [2, 4, 6, 8, 10, 12]
    

    ...我们实际上可能会得到:

    [4, 2, 8, 10, 6, 12] // or any other permutation
    
  • concatMap

    ...输入concatMap。此运算符可确保所有可观察对象均按原始顺序连接在一起,以便:

    of(1, 2, 3, 4, 5, 6).pipe(
        concatMap(number => api.get('/double', { number }))
    );
    

    ...将始终产生:

    [2, 4, 6, 8, 10, 12]
    

    查看此处的操作https://codepen.io/anon/pen/MMLdav?editors=1010

    这就是我们想要的,但是现在请求将不会并行运行。正如the documentation所说:

      

    concatMap等效于mergeMap参数设置为concurrency的{​​{1}}。

所以回到问题:是否有可能获得1的好处,从而可以并行运行给定数量的请求,同时仍然发出映射的值按原始顺序?


我的具体问题

上面抽象地描述了该问题。当您知道眼前的实际问题时,有时更容易推理出问题,所以去了:

  1. 我有一个必须发货的订单清单:

    mergeMap
  2. 我有一个const orderNumbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 方法可以实际发送订单。它返回一个shipOrder

    Promise
  3. API最多只能同时处理5笔订单货,因此我使用const shipOrder = orderNumber => api.shipOrder(orderNumber); 映射来处理:

    merge
  4. 在发货后,我们需要打印其发货标签。我有一个from(orderNumbers).pipe( mergeMap(orderNumber => shipOrder(orderNumber), 5) ); 函数,给定已发货订单的订单号,它将打印其发货标签。因此,我订阅了我们的Observable,并在输入值时打印货运标签:

    printShippingLabel
  5. 这可行,但是现在发货标签打印不正确,因为from(orderNumbers) .pipe(mergeMap(orderNumber => shipOrder(orderNumber), 5)) .pipe(orderNumber => printShippingLabel(orderNumber)); 根据mergeMap完成请求的时间发出值。我想要的是标签以与原始列表相同的顺序打印

有可能吗?


可视化

有关此问题的可视化信息,请参见此处:https://codepen.io/anon/pen/agMWBK?editors=1010

您可以看到,早先的订单在发货之前就已经被打印出来。

3 个答案:

答案 0 :(得分:2)

我确实设法部分解决了该问题,因此我将其发布在这里,作为对自己问题的解答。

我仍然非常想知道处理这种情况的规范方法。


一个复杂的解决方案

  1. 创建一个自定义运算符,该运算符采用具有索引键的值(在 Typescript措辞中为{ index: number } ),并保留值的缓冲区,仅根据它们的值发出它们index的订单。

  2. 将原始列表映射到嵌入了index的对象列表中。

  3. 将其传递给我们的自定义sortByIndex运算符。

  4. 将值映射回其原始值。

sortByIndex如下所示:

function sortByIndex() {
    return observable => {
        return Observable.create(subscriber => {
            const buffer = new Map();
            let current = 0;
            return observable.subscribe({
                next: value => {
                    if (current != value.index) {
                        buffer.set(value.index, value);
                    } else {
                        subscriber.next(value);

                        while (buffer.has(++current)) {
                            subscriber.next(buffer.get(current));
                            buffer.delete(current);
                        }
                    }
                },
                complete: value => subscriber.complete(),
            });
        });
    };
}

有了sortByIndex运算符,我们现在可以完成整个流程:

of(1, 2, 3, 4, 5, 6).pipe(
    map((number, index) => ({ number, index })),
    mergeMap(async ({ number, index }) => {
        const doubled = await api.get('/double', { number });
        return { index, number: doubled };
    }, 3),
    sortByIndex(),
    map(({ number }) => number)
);

查看此处的操作https://codepen.io/anon/pen/VJgOeN?editors=1010

创建一个concurrentConcat运算符

事实上,有了这个sortByIndex运算符之后,我们现在可以创建一个通用的concurrentConcat运算符,该运算符将在内部进行往返{ index: number, value: T }类型的转换:

function concurrentConcat(mapper, parallel) {
    return observable => {
        return observable.pipe(
            mergeMap(
                mapper,
                (_, value, index) => ({ value, index }),
                parallel
            ),
            sortByIndex(),
            map(({ value }) => value)
        );
    };
}

然后我们可以使用此concurrentConcat运算符代替mergeMap,它现在将按其原始顺序发出值:

of(1, 2, 3, 4, 5, 6).pipe(
    concurrentConcat(number => api.get('/double', { number }), 3),
);

查看此处的操作https://codepen.io/anon/pen/ZdwNXx?editors=1010

所以要解决我原来的订单发货问题:

from(orderNumbers)
    .pipe(concurrentConcat(orderNumber => shipOrder(orderNumber), maxConcurrent))
    .subscribe(orderNumber => printShippingLabel(orderNumber));

查看此处的操作https://codepen.io/anon/pen/QXRVxY?editors=1010

您可以看到,即使以后的订单可能最终要比以前的订单早发货,但标签始终按照原始订单打印。


结论

该解决方案甚至还不完整(因为它不处理发出多个值的内部可观察对象),但是它需要一堆自定义代码。这是一个普遍的问题,我觉得必须有一种更简单的(内置)方式来解决这个问题:|

答案 1 :(得分:1)

您可以使用以下运算符:sortedMergeMapexample

const DONE = Symbol("DONE");
const DONE$ = of(DONE);
const sortedMergeMap = <I, O>(
  mapper: (i: I) => ObservableInput<O>,
  concurrent = 1
) => (source$: Observable<I>) =>
  source$.pipe(
    mergeMap(
      (value, idx) =>
        concat(mapper(value), DONE$).pipe(map(x => [x, idx] as const)),
      concurrent
    ),
    scan(
      (acc, [value, idx]) => {
        if (idx === acc.currentIdx) {
          if (value === DONE) {
            let currentIdx = idx;
            const valuesToEmit = [];
            do {
              currentIdx++;
              const nextValues = acc.buffer.get(currentIdx);
              if (!nextValues) {
                break;
              }
              valuesToEmit.push(...nextValues);
              acc.buffer.delete(currentIdx);
            } while (valuesToEmit[valuesToEmit.length - 1] === DONE);
            return {
              ...acc,
              currentIdx,
              valuesToEmit: valuesToEmit.filter(x => x !== DONE) as O[]
            };
          } else {
            return {
              ...acc,
              valuesToEmit: [value]
            };
          }
        } else {
          if (!acc.buffer.has(idx)) {
            acc.buffer.set(idx, []);
          }
          acc.buffer.get(idx)!.push(value);
          if (acc.valuesToEmit.length > 0) {
            acc.valuesToEmit = [];
          }
          return acc;
        }
      },
      {
        currentIdx: 0,
        valuesToEmit: [] as O[],
        buffer: new Map<number, (O | typeof DONE)[]>([[0, []]])
      }
    ),
    mergeMap(scannedValues => scannedValues.valuesToEmit)
  );

答案 2 :(得分:0)

您想要的是什么

from(orderNumbers)
  .pipe(map(shipOrder), concatAll())
  .subscribe(printShippingLabel)

说明:

管道中的第一个运算符是 map 。它将立即为每个值调用 shipOrder (这样后续的值可能会开始并行请求)。

第二个运算符 concatAll 将已解析的值按正确的顺序放置。

(我简化了代码; concatAll()等同于concatMap(identity)。)