如何防止从史诗开始两次相同的调用

时间:2018-03-29 18:44:38

标签: javascript redux rxjs redux-observable

我的史诗会在调度REMOTE_DATA_STARTED操作时醒来,并使用action.urlaction.owner获取数据。

我需要确保不会对同一所有者/网址发起两次并发调用。完成对所有者/网址的调用后,可以稍后为同一所有者/网址启动另一个。

取消不是我在这里寻找的,因为我不想取消现有的请求,我想阻止开始新的请求。

我觉得我需要混合exhaustMapgroupBy,但我不知道从哪里开始。

这是我的史诗,它拒绝所有并发呼叫,而不是所有者/网址

const myEpic = action$ =>
  action$.ofType("REMOTE_DATA_STARTED").exhaustMap(action =>
    fakeAjaxCall().map(() => {
      return { type: "COMPLETED", owner: action.owner, url: action.url };
    })
  );

试一试

我使用失败的测试用例创建了此测试项目。你能帮我做这个吗?

https://codesandbox.io/s/l71zq6x8zl

正如您所见,test1_exhaustMapByActionType_easy工作正常,test2_exhaustMapByActionTypeOwnerAndUrl失败。

确保展开控制台以查看测试结果。

3 个答案:

答案 0 :(得分:2)

使用groupBy& amp; expandstMap以优雅的方式:

const { delay, groupBy, mergeMap, exhaustMap } = Rx.operators;

const groupedByExhaustMap = (keySelector, project) => 
  source$ => source$.pipe(
    groupBy(keySelector),
    mergeMap(groupedCalls => 
      groupedCalls.pipe(
        exhaustMap(project)
      )
    )
  );

const calls = [ // every call takes 500ms
  {startTime: 0, owner: 1, url: 'url1'}, 
  {startTime: 200, owner: 2, url: 'url2'},
  {startTime: 400, owner: 1, url: 'url1'}, // dropped
  {startTime: 400, owner: 1, url: 'url2'},
  {startTime: 600, owner: 1, url: 'url1'}
];

const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

const simulateCallsOverTime$ = Rx.Observable.from(calls)  
  .pipe(
    mergeMap(call => Rx.Observable.of(call)
      .pipe(
        delay(call.startTime)
      )
    )
  );

simulateCallsOverTime$
  .pipe(
    groupedByExhaustMap(
      call => `${call.owner}_${call.url}`,
      async call => {
        await sleep(500); // http call goes here
        return call;
      }
    )
  )
  .subscribe(console.log);



<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.8/Rx.js"></script>
&#13;
using System.Web.Security;
using WebMatrix.WebData;
&#13;
&#13;
&#13;

答案 1 :(得分:2)

有很多方法可以解决问题。我的第一个想法是,您可以将操作拆分为所有者+ URL主题,然后使用这些:

const myEpic = (action$) => {
    const completed$ = new Subject();
    const flights = new DefaultMap((pair$) =>
        pair$.exhaustMap((action) =>
            fakeAjaxCall().map(() => ({
                ...action,
                type: 'COMPLETED',
            }))
        )
        .subscribe((action) => completed$.next(action))
    );
    action$.ofType('REMOTE_DATA_STARTED')
        .subscribe((action) => {
            flights.get(`${action.owner}+${action.url}`).next(action);
        });

    return completed$;
};

这是有效的,但不可否认,它需要维护某种“默认地图”,其中新的所有者+ URL对获得新的Subject(我写了一个快速实现)。它通过的测试用例:

test('myEpic does both drop actions and NOT drop actions for two owner+url pairs', async () => {
    const arrayOfAtMost = (action$, limit) => action$.take(limit)
        .timeoutWith(1000, Observable.empty())
        .toArray().toPromise();
    const action$ = new ActionsObservable(
        Observable.create((observer) => {
            // Jim #1 emits four (4) concurrent calls—we expect only two to be COMPLETED, one per URL
            observer.next({ type: 'REMOTE_DATA_STARTED', url: 'google.com', owner: 'jim1' });
            observer.next({ type: 'REMOTE_DATA_STARTED', url: 'google.com', owner: 'jim1' });
            observer.next({ type: 'REMOTE_DATA_STARTED', url: 'google.org', owner: 'jim1' });
            observer.next({ type: 'REMOTE_DATA_STARTED', url: 'google.org', owner: 'jim1' });

            // Jim #2 emits two (2) calls at the same time as Jim #1—we expect only one to be COMPLETED, deduped URLs
            observer.next({ type: 'REMOTE_DATA_STARTED', url: 'google.biz', owner: 'jim2' });
            observer.next({ type: 'REMOTE_DATA_STARTED', url: 'google.biz', owner: 'jim2' });

            // Once all of the above calls are completed, Jim #1 and Jim #2 make calls simultaneously
            // We expect both to be COMPLETED
            setTimeout(() => {
                const url = 'https://stackoverflow.com/q/49563059/1267663';
                observer.next({ type: 'REMOTE_DATA_STARTED', url, owner: 'jim1' });
                observer.next({ type: 'REMOTE_DATA_STARTED', url, owner: 'jim2' });
            }, 505);
        })
    );
    const resultant$ = myEpic(action$);
    const results = await arrayOfAtMost(resultant$, 5);

    expect(results).toEqual([
        { type: 'COMPLETED', url: 'google.com', owner: 'jim1' },
        { type: 'COMPLETED', url: 'google.org', owner: 'jim1' },
        { type: 'COMPLETED', url: 'google.biz', owner: 'jim2' },
        { type: 'COMPLETED', url: 'https://stackoverflow.com/q/49563059/1267663', owner: 'jim1' },
        { type: 'COMPLETED', url: 'https://stackoverflow.com/q/49563059/1267663', owner: 'jim2' },
    ]);
});

完整的解决方案,包括DefaultMap实施:

const { Observable, Subject } = require('rxjs');

class DefaultMap extends Map {
    constructor(initializeValue) {
        super();
        this._initializeValue = initializeValue || (() => {});
    }

    get(key) {
        if (this.has(key)) {
            return super.get(key);
        }

        const subject = new Subject();
        this._initializeValue(subject);
        this.set(key, subject);
        return subject;
    }
}

const fakeAjaxCall = () => Observable.timer(500);
const myEpic = (action$) => {
    const completed$ = new Subject();
    const flights = new DefaultMap((uniquePair) =>
        uniquePair.exhaustMap((action) =>
            fakeAjaxCall().map(() => ({
                ...action,
                type: 'COMPLETED',
            }))
        )
        .subscribe((action) => completed$.next(action))
    );
    action$.ofType('REMOTE_DATA_STARTED')
        .subscribe((action) => {
            flights.get(`${action.owner}+${action.url}`).next(action);
        });

    return completed$;
};

*上面的代码段实际上并不可运行,我只是希望它可以折叠。

带有测试用例的可运行示例

I've put together a runnable example with test cases on GitHub.

使用groupByexhaustMap运营商

我用测试编写原始解决方案只是为了发现,是的,现有的运算符,您建议的groupByexhaustMap是可能的:

const myEpic = action$ =>
    action$.ofType('REMOTE_DATA_STARTED')
        .groupBy((action) => `${action.owner}+${action.url}`)
        .flatMap((pair$) =>
            pair$.exhaustMap(action =>
                fakeAjaxCall().map(() => ({
                    ...action,
                    type: 'COMPLETED',
                }))
            )
        );

针对上面相同的测试套件运行它将通过。

答案 2 :(得分:1)

好的,我们走了:

GroupBy req.owner,展平结果:

const myEpic = action$ =>
  action$
    .ofType("REMOTE_DATA_STARTED")
    .groupBy(req => req.owner)
    .flatMap(ownerGroup => ownerGroup.groupBy(ownerReq => ownerReq.url))
    .flatMap(urlGroup => 
      urlGroup.exhaustMap(action => 
        fakeAjaxCall().map(() => ({ type: "COMPLETED", owner: action.owner, url: action.url }))
      )
    )

不要忘记observe.complete();

const test1_exhaustMapByActionType_easy = () => {
  const action$ = new ActionsObservable(
    Observable.create(observer => {
      observer.next({ type: "REMOTE_DATA_STARTED", owner: "ownerX", url: "url1" });
      observer.next({ type: "REMOTE_DATA_STARTED", owner: "ownerX", url: "url1" });
      setTimeout(() => {
        observer.next({ type: "REMOTE_DATA_STARTED", owner: "ownerX", url: "url1" });
        observer.complete();
      }, 30);
    })
  );

  const emittedActions = [];
  const epic$ = myEpic(action$);

  epic$.subscribe(action => emittedActions.push(action), null, () => expect("test1_exhaustMapByActionType_easy", 2, emittedActions));
};

同样在这里:

const test2_exhaustMapByActionTypeOwnerAndUrl = () => {
  const action$ = new ActionsObservable(
    Observable.create(observer => {
      // owner1 emmits 4 concurrent calls, we expect only two to COMPLETED actions; one per URL:
      observer.next({ type: "REMOTE_DATA_STARTED", owner: "owner1", url: "url1" });
      observer.next({ type: "REMOTE_DATA_STARTED", owner: "owner1", url: "url1" });
      observer.next({ type: "REMOTE_DATA_STARTED", owner: "owner1", url: "url2" });
      observer.next({ type: "REMOTE_DATA_STARTED", owner: "owner1", url: "url2" });

      // owner2 emmits 2 calls at the same time as owner 1. because the two calls
      // from owner2 have the same url, we expecty only one COMPLETED action
      observer.next({ type: "REMOTE_DATA_STARTED", owner: "owner2", url: "url1" });
      observer.next({ type: "REMOTE_DATA_STARTED", owner: "owner2", url: "url1" });

      // Once all of the above calls are completed each owner makes one concurrent call
      // we expect each call to go throught and generate a COMPLETED action
      setTimeout(() => {
        observer.next({ type: "REMOTE_DATA_STARTED", owner: "owner1", url: "url1" });
        observer.next({ type: "REMOTE_DATA_STARTED", owner: "owner2", url: "url1" });
        observer.complete();
      }, 30);
    })
  );

  const emittedActions = [];
  const epic$ = myEpic(action$);

  epic$.subscribe(action => emittedActions.push(action), null, () => expect("test2_exhaustMapByActionTypeOwnerAndUrl", 5, emittedActions));
};

Full sample