我的史诗会在调度REMOTE_DATA_STARTED
操作时醒来,并使用action.url
和action.owner
获取数据。
我需要确保不会对同一所有者/网址发起两次并发调用。完成对所有者/网址的调用后,可以稍后为同一所有者/网址启动另一个。
取消不是我在这里寻找的,因为我不想取消现有的请求,我想阻止开始新的请求。
我觉得我需要混合exhaustMap
和groupBy
,但我不知道从哪里开始。
这是我的史诗,它拒绝所有并发呼叫,而不是所有者/网址
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
失败。
确保展开控制台以查看测试结果。
答案 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;
答案 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.
q49563059.js
是史诗q49563059.test.js
包含测试用例groupBy
和exhaustMap
运营商我用测试编写原始解决方案只是为了发现,是的,现有的运算符,您建议的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',
}))
)
);
针对上面相同的测试套件运行它将通过。
答案 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));
};