How do I blend a promise with an observable? - rxjs

I'm having trouble promises and observables. I have a handful of http requests which are defined in a package using promises. In the rest of my code I am using observables for various things, including other http calls. In one particular section I am checking to see if the user's bearer token is expired and if so then I get a new token and then proceed with the rest of the call.
if (!token || token.exp < Math.round((new Date()).getTime() / 1000)) {
from(this._store.refreshBearerToken())
.pipe(flatMap(resp => {
let newToken = resp.data;
newToken.exp = (new Date()).getTime() / 1000 + newToken.expires_in;
localStorage.setItem('token', JSON.stringify(newToken))
options = options || {};
options.headers = new HttpHeaders({
"Authorization": `${newToken.token_type} ${newToken.access_token}`,
"Content-Type": "application/json"
});
return this._http$.request<T>(method, url, options as Object).pipe(share());
}));
}
Bearer Token method:
async refreshBearerToken() {
const response = await this._q2.sources.requestExtensionData({
route: "refreshBearerToken"
});
console.log(response);
return response;
}
Since this._store.refreshBearerToken returns a promise I wrapped the call in a from to convert it to an observable. This compiles but when it runs I get "Cannot read property 'pipe' of undefined".
How can I convert this promise to an observable so that I can refresh the token and then continue with the rest of the call?
Edit:
I am importing from via import { Observable, from } from "rxjs";.
So, I thought the error was coming from the line .pipe(flatMap(resp =>... but I was wrong. The error is coming from the method which is calling this.
GetInitialLinkList(): Observable<Institution[]>
{
let base = { 'MemberId': localStorage.getItem('memberId') };
let ins = localStorage.getItem("initialInstitutionList");
if (ins)
{
return of(JSON.parse(ins));
}
return this._settingsService.get().pipe(
flatMap(settings =>
{
this._settings = settings;
return this._api.request<Institution[]>("Post", `${this._settings.mea}/GetInitialLinkList`, { body: base })
.pipe(
retry(1),
catchError(this.handleError)
)
.pipe(flatMap(instList =>
{
localStorage.setItem("initialInstitutionList", JSON.stringify(instList));
return of(instList);
}))
}));
}
and that is being subscribed to inside my component:
private GetLinkList()
{
this.showWaiting.emit(true);
this._data.GetInitialLinkList().subscribe((result) =>
{
this.initialList = result;
this.showWaiting.emit(false);
});
}
From what Brandon said (I forgot to return /facepalm...) I added the return so I have return from(this._store.refreshBearerToken()) which changed my error to
ERROR Error Code: undefined
Message: You provided an invalid object where a stream was expected. You can provide an Observable, Promise, Array, or Iterable.
defaultErrorLogger # core.js:6014

Can you show the actual error and the line in the code that the error occurs on? Also show where and how you import from.
I notice your code snippet does not return the observable it builds up via from(...).pipe(...) nor does it subscribe to it. It might help to show how your code actually uses this observable.

Related

Resending a graphql mutation after re-authenticating using Apollo's useMutation

I have an issue where we're using apollo client and specifically the useMutation react hook to perform mutation calls to our GraphQL Server.
At certain times, the server may return a 401 unauthorized response - at which point, we can make a call to special endpoint which re-authenticates the client and refreshes the cookie/token whatever.
I want to be able to re-run the same mutation again once the client is re-authenticated. So basically I would like to know if it is possible to do the following:
useMutation --> Receive 401 Unauthorized --> call to refresh token --> rerun same initial mutation
This is how our useMutation looks like:
const [mutationFunction, { data, ...rest }] = useMutation(query, {
onError(_err: any) {
const networkError = error?.networkError as any;
if (networkError?.statusCode === 401 && !refreshFailed) {
// eslint-disable-next-line prefer-destructuring
loading = true;
error = undefined;
fetch('/authentication/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
})
.then(response => response.json())
.then(token => {
localStorage.setItem(jwtLocalStorageKey, token);
// re fetch here
})
.catch(() => {
refreshFailed = true;
});
} else {
showAlert(_err.message, 'error');
}
}
});
and this is how we call it currently:
const {
mutationFunction: updateTournamentUserMutation,
loading: updateTournamentUserLoading,
error: updateTournamentUserError,
data: updateTournamentUserData
} = useMutationHook(gqlUpdateTournamentUser);
updateTournamentUserMutation({ variables: { input } });
Because we're using hooks and the way we're using it above, I'm not entirely sure how we can save or reuse the same data that is initially sent in the first mutation (that is the mutation parameters)
Is it possible to do so using the current way we're doing it?

Unit testing NestJS Observable Http Retry

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)
}
})
)

redux test Actions must be plain objects. Use custom middleware for async actions

I am not using redux-thunk. this keeps error-ing and I am not sure how to fix it. The examples I see online use redux-thunk which I am not using
my repo is here and the file I am trying to test is in tests\actions\...
My action that is being called in the test
import axios from "axios";
var CancelToken = axios.CancelToken;
let fetch_cancel;
export const FETCH_CATEGORIES = "fetch_categories";
export async function fetchCategories() {
fetch_cancel && fetch_cancel();
const request = await axios.get(
`https://d1i9eedhsgvpdh.cloudfront.net/production-plentific-static/api-cache/find-a-pro/api/v1/categories/all.json`,
{
cancelToken: new CancelToken(function executor(c) {
// An executor function receives a cancel function as a parameter
fetch_cancel = c;
})
}
);
return {
type: FETCH_CATEGORIES,
payload: request
};
}
The error message means that your actions must be plain objects. For example:
store.dispatch({
type: 'ADD_TODO',
text: 'Understand the middleware'
})
If you make an async request, you can't just return an object, because you need to wait for the request to finish. If you return too early, you return a Promise.
However, I cannot reproduce your error in your sandbox.

Promise resolves before looping calls to provider finish

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.

Axios Reponse Interceptor : unable to handle an expired refresh_token (401)

I have the following interceptor on my axios reponse :
window.axios.interceptors.response.use(
response => {
return response;
},
error => {
let errorResponse = error.response;
if (errorResponse.status === 401 && errorResponse.config && !errorResponse.config.__isRetryRequest) {
return this._getAuthToken()
.then(response => {
this.setToken(response.data.access_token, response.data.refresh_token);
errorResponse.config.__isRetryRequest = true;
errorResponse.config.headers['Authorization'] = 'Bearer ' + response.data.access_token;
return window.axios(errorResponse.config);
}).catch(error => {
return Promise.reject(error);
});
}
return Promise.reject(error);
}
);
The _getAuthToken method is :
_getAuthToken() {
if (!this.authTokenRequest) {
this.authTokenRequest = window.axios.post('/api/refresh_token', {
'refresh_token': localStorage.getItem('refresh_token')
});
this.authTokenRequest.then(response => {
this.authTokenRequest = null;
}).catch(error => {
this.authTokenRequest = null;
});
}
return this.authTokenRequest;
}
The code is heavily inspired by https://github.com/axios/axios/issues/266#issuecomment-335420598.
Summary : when the user makes a call to the API and if his access_token has expired (a 401 code is returned by the API) the app calls the /api/refresh_token endpoint to get a new access_token. If the refresh_token is still valid when making this call, everything works fine : I get a new access_token and a new refresh_token and the initial API call requested by the user is made again and returned correctly.
The problem occurs when the refresh_token has also expired.
In that case, the call to /api/refresh_token returns a 401 and nothing happens. I tried several things but I'm unable to detect that in order to redirect the user to the login page of the app.
I found that in that case the if (!this.authTokenRequest) statement inside the _getAuthToken method returns a pending Promise that is never resolved. I don't understand why this is a Promise. In my opinion it should be null...
I'm a newbie with Promises so I may be missing something !
Thanks for any help !
EDIT :
I may have found a way much simpler to handle this : use axios.interceptors.response.eject() to disable the interceptor when I call the /api/refresh_token endpoint, and re-enable it after.
The code :
createAxiosResponseInterceptor() {
this.axiosResponseInterceptor = window.axios.interceptors.response.use(
response => {
return response;
},
error => {
let errorResponse = error.response;
if (errorResponse.status === 401) {
window.axios.interceptors.response.eject(this.axiosResponseInterceptor);
return window.axios.post('/api/refresh_token', {
'refresh_token': this._getToken('refresh_token')
}).then(response => {
this.setToken(response.data.access_token, response.data.refresh_token);
errorResponse.config.headers['Authorization'] = 'Bearer ' + response.data.access_token;
this.createAxiosResponseInterceptor();
return window.axios(errorResponse.config);
}).catch(error => {
this.destroyToken();
this.createAxiosResponseInterceptor();
this.router.push('/login');
return Promise.reject(error);
});
}
return Promise.reject(error);
}
);
},
Does it looks good or bad ? Any advice or comment appreciated.
Your last solution looks not bad. I would come up with the similar implementation as you if I were in the same situation.
I found that in that case the if (!this.authTokenRequest) statement inside the _getAuthToken method returns a pending Promise that is never resolved. I don't understand why this is a Promise. In my opinion it should be null...
That's because this.authTokenRequest in the code was just assigned the Promise created from window.axios.post. Promise is an object handling kind of lazy evaluation, so the process you implement in then is not executed until the Promise was resolved.
JavaScript provides us with Promise object as kind of asynchronous event handlers which enables us to implement process as then chain which is going to be executed in respond with the result of asynchronous result. HTTP requests are always inpredictable, because HTTP request sometimes consumes much more time we expect, and also sometimes not. Promise is always used when we use HTTP request in order to handle the asynchronous response of it with event handlers.
In ES2015 syntax, you can implement functions with async/await syntax to hanle Promise objects as it looks synchronous.

Resources