I'm using Saga's takeLatest to abort all requests except the latest. This works fine, but now I want to only abort requests which don't have identical url, params, and method.
I know Saga uses the type attribute to compare actions (just like vanilla Redux does), but I've also added url, params, and method to my actions because I was hoping there there was some way to do something like
yield takeLatestIf((action, latestAction) => {
const sameType = action.type === latestAction.type;
const sameUrl = action.url === latestAction.type;
const sameParams = areEqual(action.params, lastAction.params);
const sameMethod = action.method === lastAction.method;
return sameType && sameUrl && sameParams && sameMethod;
});
which should only abort requests if all 4 of those attribute comparisons are false.
How can I accomplish this?
If I get it right from your question, you want this:
Like standard takeLatest().
But when a duplicate request is made, ignore it and wait for the one already executing (a reasonable use case).
So I took takeLatest() implementation provided in the docs and adapted it to your scenario:
const takeLatestDeduped = (patternOrChannel, compareActions, saga, ...args) => fork(function*() {
let lastTask
let lastAction
while (true) {
const action = yield take(patternOrChannel)
// New logic: ignore duplicate request
if (lastTask && lastTask.isRunning() && !compareActions(lastAction, action)) {
continue
}
if (lastTask) {
yield cancel(lastTask)
}
lastTask = yield fork(saga, ...args.concat(action))
// New logic: save last action
lastAction = action
}
})
We have three cases:
No running task: start the new one - standard behavior
Running task, got non-duplicate: cancel old one, start new one - standard behavior
Running task, got duplicate: ignore - new custom hehavior
So I added case #3 logic:
Ignoring duplicate request (nothing should be done in this case, so I continue to handling next action).
Saving last action for future duplicate check.
Related
I need to load a dayjs locale dynamically on the client side. On the server, I can just require it and it works, but it will always lead to a hydration mismatch because there's no way on the client to wait until the
import(`dayjs/locale/${locale}.js`)
actually completes. Can I somehow tell next to wait for the import before beginning re-hydration on the client (since the server-rendered html is actually correct and rendered with the correct locale)?
I know it has been too long and probably you found the solution but here it is a custom hook I wrote which accepts a callback as an argument and will be called only once before rendering the component. and also returns whatever you return in the callback.
the code is simple. you can read and understand it or even improve it.
the code is right here. the gist page
the typescript code if the link gets broken in future:
const useComponentWillMount = <T>(cb: () => T): T => {
const isMountedRef = useRef(false)
const resultRef = useRef<T>()
if (!isMountedRef.current && typeof window !== "undefined") {
resultRef.current = cb()
}
isMountedRef.current = true
return resultRef.current
}
Is there a possibility to specify whether the action has its error field set to true?
const response = function*() {
yield takeEvery("CLIENT_RESPONSE", handleResponse);
}
However, we don't know whether the action with type CLIENT_RESPONSE has its error field set to true or not.
I know I can check this in the handleResponse but that seems to be more work than it should. For instance, the handleResponse might get complex because for both the non-error and error case I need to write a lot of code (i.e. I want to have different handlers for both cases).
So is there a way to specify to only take that action when error is set to true?
According to Saga API reference, the pattern (first argument) of takeEvery can be String, Array or Function.
You can achieve what you want by passing a function:
const response = function*() {
yield takeEvery(action => (action.type === "CLIENT_RESPONSE" && !action.error), handleResponse);
}
I want to know that do we really need .map when calling any api using http in Angular 2?Please check my below code. It is working fine with .map and even without .map. If api returns data then it will return success else it will return error. I will also return any model data from here after performing some action. So, do I need Observable ? Is there any benefit of using it ? I am using .subscribe at component side to receive data. Is this fine or do I need any improvement ?
returnData: ReturnData;
callyAPI(body: modelData) {
return this.http.post(URL, body)
.do(data => {
for (let i = 0; i < data.length; ++i) {
this.returnData.push(data[i]);
}
return this.returnData;
},
error => {});
});
}
You don't need to use map but do is definitly the wrong operator here
do is supposed to execute some code for every event, but not to modify the events value, while map can update or replace the event by a different value like you do in your example.
https://github.com/ReactiveX/rxjs/blob/master/src/operator/do.ts#L13-L14
Perform a side effect for every emission on the source Observable, but return
an Observable that is identical to the source.
Context
I have a basic PipeTransform, expect the fact that it is async. Why? because I have my own i18n service (because of parsing, pluralization and other constraints, I did my own) and it returns a Promise<string>:
#Pipe({
name: "i18n",
pure: false
})
export class I18nPipe implements PipeTransform {
private done = false;
constructor(private i18n:I18n) {
}
value:string;
transform(value:string, args:I18nPipeArgs):string {
if(this.done){
return this.value;
}
if (args.plural) {
this.i18n.getPlural(args.key, args.plural, value, args.variables, args.domain).then((res) => {
this.value = res;
this.done = true;
});
}
this.i18n.get(args.key, value, args.variables, args.domain).then((res) => {
this.done = true;
this.value = res;
});
return this.value;
}
}
This pipe works well, because the only delayed call is the very first one (the I18nService uses lazy loading, it loads JSON data only if the key is not found, so basically, the first call will be delayed, the other ones are instant but still async).
Problem
I can't figure out how to test this pipe using Jasmine, since it is working inside a component I know it works, but the goal here is to get this fully tested using jasmine, this way I can add it to a CI routine.
The above test:
describe("Pipe test", () => {
it("can call I18n.get.", async(inject([I18n], (i18n:I18n) => {
let pipe = new I18nPipe(i18n);
expect(pipe.transform("nope", {key: 'test', domain: 'test domain'})).toBe("test value");
})));
});
Fails because since the result given by the I18nService is async, the returned value is undefined in a sync logic.
I18n Pipe test can call I18n.get. FAILED
Expected undefined to be 'test value'.
EDIT: One way to do it would be to use setTimeout but I want a prettier solution, to avoid adding setTimeout(myAssertion, 100) everywhere.
Use fakeAsync from #angular/core/testing. It allows you to call tick(), which will wait for all currently queued asynchronous tasks to complete before continuing. This gives the illusion of the actions being synchronous. Right after the call to tick() we can write our expectations.
import { fakeAsync, tick } from '#angular/core/testing';
it("can call I18n.get.", fakeAsync(inject([I18n], (i18n:I18n) => {
let pipe = new I18nPipe(i18n);
let result = pipe.transform("nope", {key: 'test', domain: 'test domain'});
tick();
expect(result).toBe("test value");
})));
So when should we use fakeAsync and when should we use async? This is the rule of thumb that I go by (most of the time). When we are making asynchronous calls inside the test, this is when we should use async. async allows to test to continue until all asynchronous calls are complete. For example
it('..', async(() => {
let service = new Servce();
service.doSomething().then(result => {
expect(result).toBe('hello');
});
});
In a non async test, the expectation would never occur, as the test would complete before the asynchronous resolution of the promise. With the call to async, the test gets wrapped in a zone, which keeps track of all asynchronous tasks, and waits for them to complete.
Use fakeAsync when the asynchronous behavior is outside the control of the test (like in your case is going on in the pipe). Here we can force/wait for it to complete with the call to tick(). tick can also be passed a millisecond delay to allow more time to pass if needed.
Another option is to mock the service and make it synchronous, as mentioned in this post. When unit testing, if your components in test are dependent on heavy logic in the service, then the component in test is at the mercy of that service working correctly, which kinda defeats the purpose of a "unit" test. Mocking makes sense in a lot of cases.
I'm trying to do this relatively complex operation in BaconJs.
Basically, the idea is keep trying each check until you have a 'pass' status or they all fail. The catch is that 'pending' statuses have a list of Observables (built from jquery ajax requests) that will resolve the check. For performance reasons, you need to try each Observable in order until either they all pass or one fails.
Here's the full pseudo algorithm:
Go thru each check. A check contains an id and status = fail/pass/pending. If pending, it contains a list of observables.
If status = pass, then return the id (you're done!)
if status = fail, then try the next check
if status = pending
try each observable in order
if observable result is 'false', then try the next check
if reach end of observable list and result is 'true', then return the id (you're done!)
Here's the Bacon code. It doesn't work when the Observables are Ajax requests.
Basically, what happens is that it skips over pending checks....it doesn't wait for the ajax calls to return. If I put a log() right before the filter(), it doesn't log pending requests:
Bacon.fromArray(checks)
.flatMap(function(check) {
return check.status === 'pass' ? check.id :
check.status === 'fail' ? null :
Bacon.fromArray(check.observables)
.flatMap(function(obs) { return obs; })
.takeWhile(function(obsResult) { return obsResult; })
.last()
.map(function(obsResult) { return obsResult ? check.id : null; });
})
.filter(function(contextId) { return contextId !== null; })
.first();
UPDATE: the code works when the checks look like this: [fail, fail, pending]. But it doesn't work when the checks look like this: [fail, pending, pass]
I am more familiar with RxJS than Bacon, but I would say the reason you aren't seeing the desired behavior is because flatMap waits for no man.
It passes [fail, pending, pass] in quick succession, fail returns null and is filtered out. pending kicks off an observable, and then receives pass which immediately returns check.id (Bacon may be different, but in RxJS flatMap won't accept a single value return). The check.id goes through filter and hits first at which point it completes and it just cancels the subscription to the ajax request.
A quick fix would probably be to use concatMap rather than flatMap.
In RxJS though I would refactor this to be (Disclaimer untested):
Rx.Observable.fromArray(checks)
//Process each check in order
.concatMap(function(check) {
var sources = {
//If we pass then we are done
'pass' : Rx.Observable.just({id : check.id, done : true}),
//If we fail keep trying
'fail' : Rx.Observable.just({done : false}),
'pending' : Rx.Observable.defer(function(){ return check.observables;})
.concatAll()
.every()
.map(function(x) {
return x ? {done : true, id : check.id} :
{done : false};
})
};
return Rx.Observable.case(function() { return check.status; }, sources);
})
//Take the first value that is done
.first(function(x) { return x.done; })
.pluck('id');
What the above does is:
Concatenate all of the checks
Use the case operator to propagate instead of nested ternaries.
Fail or pass fast
If pending create a flattened observable out of check.observables, if they are all true then we are done, otherwise continue to the next one
Use the predicate value of first to get the first value returned that is done
[Optionally] strip out the value that we care about.
I agree with #paulpdaniels Rx-based answer. The problem seems to be that when using flatMap, Bacon.js won't wait for your first "check-stream" to complete before launching a new one. Just replace flatMap with flatMapConcat.
Thanks to #raimohanska and #paulpdaniels. The answer is to use #flatMapConcat. This turns what is basically a list of async calls done in parallel into a sequence of calls done in order (and note that the last "check" is programmed to always pass so that this always outputs something):
Bacon.fromArray(checks)
.flatMapConcat(function(check) {
var result = check();
switch(result.status) {
case 'pass' :
case 'fail' :
return result;
case 'pending' :
return Bacon.fromArray(result.observables)
.flatMapConcat(function(obs) { return obs; })
.takeWhile(function(obsResult) { return obsResult.result; })
.last()
.map(function (obsResult) { return obsResult ? {id: result.id, status: 'pass'} : {status: 'fail'}; });
}
})
.filter(function(result) { return result.status === 'pass'; })
.first()
.map('.id');