I would like to create an observable which loads data from backend. However, I would like to tell him to really fetch it or take the results from cache.
backendData$ = http.get(...).pipe(shareReplay(1));
refresh$: Subject<{doBackendRequest: boolean}> = new Subject();
rowData$ = this.refresh$.pipe(
tap(opts => console.log(opts.doBackendRequest)),
switchMap(() => {
if (opts.doBackendRequest) {
// lets do a new request
return backendData$;
} else {
// somehow return cached results
return CACHED_DATA;
}
})
The rowData$ is subscribed by an async pipe rowData$ | async
Here is how I would like to call it:
this.refresh$.next({performBackendRequest: true});
this.refresh$.next({performBackendRequest: false});
The question now is how can I cache the http result? I tried to use shareReplay(1) but I can not find a working solution. Can you point me the direction?
Here's an example of how it might be implemented:
backendData$ = http.get(...);
cachedData$ = backendData$.pipe(shareReplay(1));
refresh$: Subject<boolean> = new Subject();
rowData$ = this.refresh$
.pipe(switchMap(refresh => refresh ? backendData$ : cachedData$))
Related
I have been trying to grab an old response to assert it has a certain response.
The issue is that the same call is posted at the same time and I can only grab the second response.
I was just wondering if there was a way to grab both responses so I can read each body to make sure the correct posts are made
I have used the following
public assertMixPanelCall(call: string): void {
cy.intercept('POST', 'https://api-js.mixpanel.com/track/*', (req) => {
if (atob(req.body.replace('data=', '')).includes(`"event": "${call}"`)) {
req.alias = 'correctBody'
}
});
cy.wait('#correctBody');
}
So the response I get is the last response,
But I want to grab the penultimate response
I'm not seeing the complete picture, but I think you can use this pattern Intercepting a response
let responseCount = 0;
cy.intercept('POST', 'https://api-js.mixpanel.com/track/*', (req) => {
if (atob(req.body.replace('data=', '')).includes(`"event": "${call}"`)) {
req.continue((res) => {
responseCount++;
if (responseCount === 1) {
req.alias = 'penultimate'
}
if (responseCount === 2) {
req.alias = 'final'
}
})
}
});
cy.wait('#penultimate')
Not sure if dynamic aliasing works on a per-response basis.
There's also an undocumented alias suffix that lets you access the nth response
cy.wait('#correctBody'); // must wait first
cy.get('#correctBody.1'); // then get to access response history
// - not sure if you need #correctBody.0 or #correctBody.1
But I can't see why cy.wait('#correctBody') doesn't catch the first response, generally you need to issue the wait twice to get both responses. Anyway, there's some things to try out here.
So I found the solution
From wherever I want to start capturing
cy.intercept('POST', 'https://api-js.mixpanel.com/track/*').as('call');
generate array based on the number of calls previously I wish to check
const genArr = Array.from({length:noOfCalls},(v,k)=>k+1);
const calls = [];
cy.wrap(genArr).each(() => {
calls.push(`#${call}`)
})
make the call based on the amount of times I wish to post the response
cy.wait(calls).then(differentRequests => {
differentRequests.forEach(differentRequest => {
if(atob(differentRequest.request.body.replace('data=','')).includes(call)) {
pass = true
}
});
expect(pass).to.be.true
});
}
Got the solution from here
https://medium.com/slido-dev-blog/cypress-io-is-pretty-amazing-and-if-you-havent-used-it-yet-and-need-a-jump-start-i-recommend-my-66ee6ac5f8d9
I'm making a request to a 3rd party API via NestJS's built in HttpService. I'm trying to simulate a scenario where the initial call to one of this api's endpoints might return an empty array on the first try. I'd like to use RxJS's retryWhen to hit the api again after a delay of 1 second. I'm currently unable to get the unit test to mock the second response however:
it('Retries view account status if needed', (done) => {
jest.spyOn(httpService, 'post')
.mockReturnValueOnce(of(failView)) // mock gets stuck on returning this value
.mockReturnValueOnce(of(successfulView));
const accountId = '0812081208';
const batchNo = '39cba402-bfa9-424c-b265-1c98204df7ea';
const response =client.viewAccountStatus(accountId, batchNo);
response.subscribe(
data => {
expect(data[0].accountNo)
.toBe('0812081208');
expect(data[0].companyName)
.toBe('Some company name');
done();
},
)
});
My implementation is:
viewAccountStatus(accountId: string, batchNo: string): Observable<any> {
const verificationRequest = new VerificationRequest();
verificationRequest.accountNo = accountId;
verificationRequest.batchNo = batchNo;
this.logger.debug(`Calling 3rd party service with batchNo: ${batchNo}`);
const config = {
headers: {
'Content-Type': 'application/json',
},
};
const response = this.httpService.post(url, verificationRequest, config)
.pipe(
map(res => {
console.log(res.data); // always empty
if (res.status >= 400) {
throw new HttpException(res.statusText, res.status);
}
if (!res.data.length) {
this.logger.debug('Response was empty');
throw new HttpException('Account not found', 404);
}
return res.data;
}),
retryWhen(errors => {
this.logger.debug(`Retrying accountId: ${accountId}`);
// It's entirely possible the first call will return an empty array
// So we retry with a backoff
return errors.pipe(
delayWhen(() => timer(1000)),
take(1),
);
}),
);
return response;
}
When logging from inside the initial map, I can see that the array is always empty. It's as if the second mocked value never happens. Perhaps I also have a solid misunderstanding of how observables work and I should somehow be trying to assert against the SECOND value that gets emitted? Regardless, when the observable retries, we should be seeing that second mocked value, right?
I'm also getting
: Timeout - Async callback was not invoked within the 5000ms timeout specified by jest.setTimeout.Timeout - Async callback was not invoked within the 5000ms timeout specified by jest.setTimeout.Error:
On each run... so I'm guessing I'm not calling done() in the right place.
I think the problem is that retryWhen(notifier) will resubscribe to the same source when its notifier emits.
Meaning that if you have
new Observable(s => {
s.next(1);
s.next(2);
s.error(new Error('err!'));
}).pipe(
retryWhen(/* ... */)
)
The callback will be invoked every time the source is re-subscribed. In your example, it will call the logic which is responsible for sending the request, but it won't call the post method again.
The source could be thought of as the Observable's callback: s => { ... }.
What I think you'll have to do is to conditionally choose the source, based on whether the error took place or not.
Maybe you could use mockImplementation:
let hasErr = false;
jest.spyOn(httpService, 'post')
.mockImplementation(
() => hasErr ? of(successView) : (hasErr = true, of(failView))
)
Edit
I think the above does not do anything different, where's what I think mockImplementation should look like:
let err = false;
mockImplementation(
() => new Observable(s => {
if (err) {
s.next(success)
}
else {
err = true;
s.next(fail)
}
})
)
I'm doing some application profiling on a react native application and I'm seeing pretty big differences between the request times that I'm getting using the apollo-link-logger (as well as the links I've rolled on my own) and the Android profiler's networking lane. For a ~600ms request in the profiler I'm seeing upwards of 2 seconds from the middle ware that uses the apollo link system.
There isn't much going on in my own link (below)
const metricsLink = new ApolloLink((operation, forward) => {
const { operationName } = operation
const startTime = new Date().getTime()
const observable = forward(operation)
observable.subscribe({
complete: () => {
const elapsed = new Date().getTime() - startTime
console.warn(`[METRICS][${operationName}] (${elapsed}) complete`)
}
})
return observable
})
It seems like its going to end up factoring in the time it takes apollo to manage this request chain as well. I've confirmed that this isn't a general issue with the app by fetching from other endpoints directly with fetch and comparing those times with the profiler times (which match).
Should I expect this to actually reflect the request time? Where might the rest of the time be going between the native network request and the time I'm seeing in the apollo client?
From the Apollo Link docs, there is an example to measure time using link composition
Note: I've modified the example to use performance.now() instead of new Date()based on this other SO answer
const timeStartLink = new ApolloLink((operation, forward) => {
operation.setContext({ start: performance.now() })
return forward(operation)
})
const logTimeLink = new ApolloLink((operation, forward) => {
return forward(operation).map(data => {
// data from a previous link
const time = performance.now() - operation.getContext().start
console.log(`operation ${operation.operationName} took ${time} to complete`)
return data
})
})
const link = timeStartLink.concat(logTimeLink)
The issue (at least in the version I was using) was that the Zen Observable (used by Apollo in these links) executes its logic every time the subscribe call is made. That meant that I got double requests being sent because I had two links and each one called subscribe and forwarded the already created observable. My workaround was to create new observables each time and hook them up to the observable that the prior link passed down.
const metricsLink = new ApolloLink((operation, forward) => {
const { operationName } = operation
const startTime = new Date().getTime()
const observable = forward(operation)
// Return a new observable so no other links can call .subscribe on the
// the one that we were passsed.
return new Observable(observer => {
observable.subscribe({
complete: () => {
const elapsed = new Date().getTime() - startTime
console.warn(`[METRICS][${operationName}] (${elapsed}) complete`)
observer.complete()
},
next: observer.next.bind(observer),
error: error => {
// ...
observer.error(error)
}
})
})
})
This seems like an oversight to me, but at least I have a workaround.
I have a custom service which fetches data from multiple mongoose models and creates an array using these data and gives back to API.
Everything works well.. I can console the new object-array 'result.rolePermissions'. you can see that in the code below.
My problem is when I check the data on the frontend side am getting a blank array as a response.
I have double checked if I missed a variable or used a different API call, but no. I am exactly calling this custom service function but for some reason, I can't get the data.
Is it because of the usage of await/Promise on the 'result.roles', 'result.permission'? Did I use some bad practices?
async find (params) {
let result = {};
result.roles = [];
result.rolePermissions = [];
result.permissionsModules = [];
const permissionGroupModel = this.app.service('permission-groups').Model;
const rolesModel = this.app.service('roles').Model;
const permissionsModel = this.app.service('permissions').Model;
//all permission-groups are getting fine
result.permissionsModules = await new Promise(function(resolve, reject) {
permissionGroupModel.find().populate('permissions').exec((err,response)=>{
resolve(response);
})
});
//all roles are getting fine
result.roles = await new Promise(function(resolve, reject) {
rolesModel.find().then(response=>{
resolve(response);
})
});
//all permissions are getting fine
result.permissions = await new Promise(function(resolve, reject) {
permissionsModel.find().then(response=>{
resolve(response);
})
});
//here is iam having trouble..
result.rolePermissions = await new Promise(function(resolve, reject) {
let rolePerms = [];
result.roles.forEach(role => {
rolePerms[role.name] = {};
result.permissions.forEach(permission => {
rolePerms[role.name][permission.name] = (role.permissions.indexOf(permission._id) > -1)?true:false;
});
});
resolve(rolePerms);
});
//I have consoled result.rolePermissions and it shows the currect data.but when this data getting as null array in api call response
console.log('result.rolePermissions')
console.log(result.rolePermissions)
return {
status:true,
test:data.rolePermissions,
data:result
}
}
There are a couple things that can be done to improve this code. First, anything of the form...
x = await new Promise(function(resolve, reject) {
promiseReturningFunction(params).then(response => {
resolve(response);
});
});
...can be rewritten as...
x = await promiseReturningFunction(params);
...because, when you have a promise-returning-function, there's no need to create another promise (with new Promise).
The code where you indicate trouble doesn't appear to need a promise at all...
//here is iam having trouble..
result.rolePermissions = [];
result.roles.forEach(role => {
rolePerms[role.name] = {};
result.permissions.forEach(permission => {
rolePerms[role.name][permission.name] (role.permissions.indexOf(permission._id) > -1)?true:false;
});
});
Finally, I'm puzzled by the use of data in this code...
return {
status:true,
test:data.rolePermissions,
data:result
}
Is it possible that, while your use of Promises was non-standard and confusing, that code was pretty much working, and the real problem comes from passing data.rolePermissions rather than the object you just built and tested with the log (result.rolePermissions)?
The issue was fixed when i restarted the feathers service. I am not deleting the question because the other answer may share some knowledge
I'm running into an issue with some code for an Ionic 3 app.
Basically, I have a list of objects that all have a unique id. The unique id for each object must be sent through a GET request so that we can get the appropriate data back for each object from the server. This has to be done on a per object basis; I can't bundle them into one request because there is no API endpoint for that.
Therefore the objects are all stored in an array, so I've been trying to loop through the array and call the provider for each one. Note that the provider is returning an observable.
Since the provider is an asynchronous function the promise will resolve before the loop is finished, unless I time out the promise resolution. This defeats the whole point of the promise.
What is the correct way that I should go about doing this so that the looping provider calls are done before the promise resolves?
If I have an inner promise to resolve when the looping is done, won't it also resolve prematurely?
I also read that it is bad to have a bunch of observables open. Should I instead return each observable as a promise using toPromise()?
Here is the code to build the data:
asyncBuildData() {
var promise = new Promise((resolve, reject) => {
let completedRequests = 0;
for (let i = 0; i < 10; i++) {
this.provider.getStuffById(listOfStuff[i].id).subscribe(val => {
list.push(val)
completedRequests++;
})
}
console.log('cp', completedRequests); // completedRequests = 0
setTimeout(() => {
console.log('cp', completedRequests); // completedRequests = 10
let done = true;
if (done) {
resolve('Done');
} else {
reject('Not done');
}
}, 1500)
})
return promise;
}
Code from Provider:
getStuffById(stuffId) {
let url = url + stuffId;
return this.http.get(url)
.map(res => res.json());
}
Even though you can't bundle them into one request, you can still bundle them into one observable, of which those requests are fired in parallel, using .forkJoin():
buildData$() {
let parallelRequests = listOfStuffs.map(stuff => this.provider.getStuffById(stuff.id));
return Observable.forkJoin([...parallelRequests]);
}
and then in your component, you can just call:
buildData$.subscribe(val=>{
//val is an array of all the results of this.provider.getStuffById()
list =val;
})
Note that Obersvable.forkJoin() will for all requests to complete before emitting any values.
If I understand correctly then the following code should get you on your way. This will execute a promise, one at a time, for each element in the array.
var ids = [1,2,3,4,5];
ids.reduce(function (promise, id) {
return promise.then(function () {
let url = url + id;
return this.http.get(url)
.map(res => res.json());
});
}, Promise.resolve()).then(function(last) {
// handle last result
}, function(err) {
// handle errors
});
I tested this with a jQuery post and replaced it with yours from Ionic. If it fails then let me know.