programing

에픽에서 동일한 통화를 두 번 시작하는 것을 방지하는 방법

prostudy 2022. 4. 2. 08:58
반응형

에픽에서 동일한 통화를 두 번 시작하는 것을 방지하는 방법

내 서사시는 그 때 잠에서 깨어난다.REMOTE_DATA_STARTED액션이 전송되고 다음 명령을 사용하여 데이터 가져오기action.url그리고action.owner.

동일한 소유자/url에게 두 번의 동시 통화를 시작하지 않도록 해야겠습니다.소유자/url에 대한 호출이 완료되면 나중에 동일한 소유자/url에 대해 다른 호출을 시작해도 괜찮다.

기존 요청을 취소하고 싶지 않기 때문에 취소는 여기서 찾는 것이 아니며, 새 요청을 시작하는 것을 방지하고 싶다.

나는 혼혈이 필요한 것 같다.exhaustMap그리고groupBy, 하지만 여기서 어디로 가야 할지 모르겠어.

이 시점에서 이건 내 서사시야, 모든 동시통화를 거부하지, 소유자/url에 의한 것이 아니다.

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

Try it Live

나는 실패한 시험 케이스로 이 시험 프로젯을 만들었다.내가 이 일을 할 수 있게 도와줄 수 있니?

https://codesandbox.io/s/l71zq6x8zl

보다시피test1_exhaustMapByActionType_easy잘 되네, 잘 되네.test2_exhaustMapByActionTypeOwnerAndUrl그것은 실패한다.

테스트 결과를 보려면 콘솔을 확장하십시오.

groupBy & expastMap으로 우아하게 할 수 있다.

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

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>

해결책에 접근하는 방법에는 여러 가지가 있다.내 첫 번째 생각은 네가 그 행동을 오너+로 나눌 수 있다는 것이었다.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$;
};

* 위의 조각은 실제로 실행할 수 있는 것이 아니라 접히는 것을 원했을 뿐이다.

테스트 사례와 함께 실행 가능한 예제

나는 GitHub에 대한 테스트 케이스와 함께 실행 가능한 예시를 작성했다.

사용.groupBy그리고exhaustMap운영자

나는 기존 연산자를 통해서만 가능하다는 것을 발견하기 위해 테스트와 함께 원래의 솔루션을 작성했고, 그리고 당신이 제안한 것은 다음과 같다.

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

위와 같은 테스트 세트에 대해 실행하면 합격이다.

자, 이제 시작합시다.

그룹비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));
};

전체 샘플

참조URL: https://stackoverflow.com/questions/49563059/how-to-prevent-starting-the-same-call-twice-from-epic

반응형