Intercept each request with before and after hooks - ajax

I have to make 5 requests (order doesn't matter) to 5 different endpoints. The URL of these endpoints is the same, except for the business line. These business lines are the array of the from.
I want show a skeleton loader before each request and hide once it finish. So, basically the flow is:
1. [Hook - before request]
2. [Log of data fetched]
3. [Hook - after request]
This is my service:
export function getInsurances(
userIdentity: string,
hooks?: RequestHooks
): Observable<Policy[]> {
return from(["all", "vehicle", "health", "soat", "plans"]).pipe(
tap(() => hooks?.beforeRequest && hooks.beforeRequest()),
flatMap<string, Observable<Policy[]>>(businessLine => {
return InsurancesApi.getPolicies<Policy>(
userIdentity,
businessLine
).pipe(
map(policies => {
return policies.map(policy => PolicyStandarizer(policy));
}),
finalize(() => {
hooks?.afterRequest && hooks.afterRequest();
})
);
}),
catchError(err => of(err)),
takeUntil(HttpCancelator)
);
}
This is my subscribe:
const hooks = {
beforeRequest() {
Log.info("Before Request");
setStatus(HttpRequestStatus.PENDING);
},
afterRequest() {
Log.warn("After Request");
setStatus(HttpRequestStatus.RESOLVED);
},
};
getInsurances(userIdentity, hooks).subscribe(
policies => {
Log.normal("Policies:", policies);
setInsurances(policies);
},
(err: Error) => {
setError(err);
}
);
And have this output (sorry for paste the link, I can't embed the image because rep):
https://i.stack.imgur.com/Nbq49.png
The finalize is working fine, but the tap is executing five times at once.
Thank you.

I think you get this behavior because from emits the items synchronously, so its essentially the same as doing:
for (let i = 0; i < arr.length; i++) {
console.log('before req');
observer.next(arr[i]);
}
observer.complete();
afterRequest is shown properly because the actions involved are asynchronous.
If you want to trigger that event only once, before all the requests are fired, you could try this:
from([...])
.pipe(
finalize(() => hooks?.beforeRequest && hooks.beforeRequest()),
flatMap(/* ... */)
)
EDIT - log event before each request
flatMap(
value => concat(
of(null).pipe(
tap(() => hooks?.beforeRequest && hooks.beforeRequest()),
ignoreElements(), // Not interested in this observable's values
),
InsurancesApi.getPolicies(/* ... */)
)
)

Related

Cypress test: Writing multiple elements to database failed

Trying to write multiple elements into database failed, only the last one is written:
// my_test.cy.js
import CreateProductPage from "../pages/CreateProductPage";
describe('product detail page', () => {
beforeEach(() => {
cy.login('admin', 'shop')
})
it('should print typed product', () => {
cy.createProduct(null, 'Component Product 0', '0')
CreateProductPage.elements.productDetailTabs().should('exist') // <--- to detect that the entity is written
cy.createProduct('combinable', 'Combined Test Product', '0')
CreateProductPage.elements.productDetailTabs().should('exist') // <--- to detect that the entity is written
})
})
// commands.js
Cypress.Commands.add('createProduct', (type, name, grossPrice) => {
cy.visit('/#/sw/product/create')
CreateProductPage.elements.productDetailTabs().should('not.exist').then(() => {
if(type === 'combinable') {
CreateProductPage.elements.radioBtnCombinableProduct().click()
}
CreateProductPage.elements.inputProductName().clear().type(name)
CreateProductPage.elements.inputPriceFieldGross().type(grossPrice)
SwPageHeader.elements.btnProductSave().click()
})
})
Questions:
This failed because of asynchronous nature of cypress?
If so, how to interrupt? Chaining with then(), the behavior is the same
With this is code (adding wait()) it works, but i'm looking for the right way
// my_test.cy.js
describe('product detail page', () => {
beforeEach(() => {
cy.login('admin', 'shop')
})
it('should print typed product', () => {
cy.createProduct(null, 'Component Product 0', '0')
CreateProductPage.elements.productDetailTabs().should('exist')
cy.wait(300)
cy.createProduct('combinable', 'Combined Test Product', '0')
CreateProductPage.elements.productDetailTabs().should('exist')
})
})
EDIT 1
// pages/CreateProductPage.js
class CreateProductPage {
elements = {
productDetailTabs: () => cy.get('div.sw-product-detail-page__tabs'),
radioBtnCombinableProduct: () => cy.get('.sw-product-detail-base__info input#type_combinable_product-0'),
radioBtnUnCombinableProduct: () => cy.get('.sw-product-detail-base__info input#type_combinable_product-1'),
inputProductName: () => cy.get('input#sw-field--product-name'),
inputPriceFieldGross: () => cy.get('div.sw-list-price-field__price input#sw-price-field-gross'),
}
}
module.exports = new CreateProductPage();
If the problem is one of waiting, you will need to figure out something that indicates to the user that the save was successful and test that.
For example, if there was a toast message on screen:
...
SwPageHeader.elements.btnProductSave().click()
cy.contains('span', 'Product was saved').should('be.visible')
Blockquote
This failed because of asynchronous nature of cypress?
Code behaves synchronously if you have only cy commands inside the code block.
As suggested in https://stackoverflow.com/a/74721728/9088832 you should wait for some element to appear or for an API request to be completed that is responsible for the product creation.

how to use multicasting obs with behavioursubject?

In general we need behavior subject functionality. But only on first subscription we should send subscribe to server in REST. And to send unsubscribe on the last unsubscribe, and all late observers subscribed will gwt the latest json recwived from the first. can i do it using rxjs operaTors and how? or shoul i use custom obserbale ?
currently the custom code for this is this:
public observable: Observable<TPattern> = new Observable((observer: Observer<TPattern>) => {
this._observers.push(observer);
if (this._observers.length === 1) {
this._subscription = this.httpRequestStream$
.pipe(
map((jsonObj: any) => {
this._pattern = jsonObj.Data;
return this._pattern;
})
)
.subscribe(
(data) => this._observers.forEach((obs) => obs.next(data)),
(error) => this._observers.forEach((obs) => obs.error(error)),
() => this._observers.forEach((obs) => obs.complete())
);
}
if (this._pattern !== null) {
observer.next(this._pattern); // send last updated array
}
return () => {
const index: number = this._observers.findIndex((element) => element === observer);
this._observers.splice(index, 1);
if (this._observers.length === 0) {
this._subscription.unsubscribe();
this._pattern = null; // clear pattern when unsubscribed
}
};
});
Sounds like you need a shareReplay(1), it will share the latest response with all subscribes.
const stream$ = httpRequestStream$.pipe(
shareReplay(1),
),
stream$.subscribe(); // sends the request and gets its result
stream$.subscribe(); // doesn't send it but gets cached result
stream$.subscribe(); // doesn't send it but gets cached result
stream$.subscribe(); // doesn't send it but gets cached result

RxJS throttle for AJAX requests

I want to create a function that will make AJAX requests to backend. And if this function is called many times at the same time, then it should not make many identical requests to the server. It must make only 1 request.
For example:
doAJAX('http://example-1.com/').subscribe(res => console.log); // must send a request
doAJAX('http://example-1.com/').subscribe(res => console.log); // must NOT send a request
doAJAX('http://example-2.com/').subscribe(res => console.log); // must send a request, bacause of different URL
window.setTimeout(() => {
doAJAX('http://example-2.com/').subscribe(res => console.log); // must send a request because too much time has passed since the last request
}, 3000)
All function calls should return a result, as if the request was actually made.
I think for this purpose I can use RxJS library.
I have done this:
const request$ = new Subject < string > ();
const response$ = request.pipe(
groupBy((url: string) => url),
flatMap(group => group.pipe(auditTime(500))), // make a request no more than once every 500 msec
map((url: string) => [
url,
from(fetch(url))
]),
share()
);
const doAJAX = (url: string): Observable <any> {
return new Observable(observe => {
response$
.pipe(
filter(result => result[0] === url),
first(),
flatMap(result => result[1])
)
.subscribe(
(response: any) => {
observe.next(response);
observe.complete();
},
err => {
observe.error(err);
}
);
request$.next(url);
});
}
I create request$ subject and response$ observable. doAjax function subscribes for response$ and send URL string to request$ subject. Also there are groupBy and auditTime operators in request$ stream. And filter operator in doAJAX function.
This code works but I think it is very difficult. Is there a way to make this task easier? Maybe RxJS scheduler or not use RxJS library at all
As the whole point of this is to memoize Http results and delay repeated calls, you might consider your own memoization. Example:
const memoise = (func) => {
let cache: { [key:string]: Observable<any> } = {};
return (...args): Observable<any> => {
const cacheKey = JSON.stringify(args)
cache[cacheKey] = cache[cacheKey] || func(...args).pipe(share());
return cache[cacheKey].pipe(
tap(() => timer(1000).subscribe(() => delete cache[cacheKey]))
);
}
}
Here is a Stackblitz DEMO

rxjs: how to order responses via Observables

I am using socket.io to send a series of responses to my front-end. The responses are intended to be sequential, but depending on the connection created by socket.io they're not always guaranteed to come in the correct order (https://github.com/josephg/ShareJS/issues/375).
Assuming each response had a sequence field that held a number (shown as the number in the picture above), the observable should emit these responses in order.
If a response is received out of order and a certain amount of time (n) passes without getting any response, I would like for my observable to emit an error, to signal to my front-end to reset the connection.
A really nice problem. Below a snippet with most important parts commented.
// mock ordered values
const mockMessages = Rx.Observable.fromEvent(document.querySelector('#emit'), 'click')
.map((e, index) => ({
index,
timestamp: e.timeStamp
}))
.delayWhen(() => Rx.Observable.timer(Math.random() * 2000)) // distort order
// there is a lot of mutability in `keepOrder`, but all of it
// is sealed and does not leak to outside environment
const keepOrder = timeoutMs => stream =>
Rx.Observable.defer(() => // need defer to support retries on error
stream.scan((acc, v) => {
acc.buffer.push(v)
acc.buffer.sort((v1, v2) => v1.index - v2.index)
return acc
}, {
lastEmitted: -1,
buffer: []
})
.mergeMap(info => {
const emission = []
while (info.buffer.length && info.lastEmitted + 1 === info.buffer[0].index) {
emission.push(info.buffer.shift())
info.lastEmitted += 1
}
return Rx.Observable.of(emission)
})
.switchMap(emissions => {
if (!emissions.length) { // this condition indicates out of order
return Rx.Observable.timer(timeoutMs)
.mergeMapTo(Rx.Observable
.throw(new Error('ORDER_TIMEOUT')))
} else {
return Rx.Observable.from(emissions)
}
})
)
mockMessages
.do(x => console.log('mocked', x.index))
.let(keepOrder(1000)) // decrease timeoutMs to increase error probablity
.do(x => console.log('ORDERED', x.index))
.retryWhen(es => es
.do(e => console.warn('ERROR', e)))
.subscribe()
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.2/Rx.js"></script>
<button id="emit">EMIT</button>

How to queue actions through time properly when using redux-observables

I'm trying to create an Epic that queues actions to be dispatched, in this case, the actions are messages to be displayed with a snackbar so that, for example, three errors happened almost simultaneously, and I want to display the three error messages with a SnackBar, the Epic should dispatch an action to display the first one for 3 seconds, then display the second one for 3 seconds and then the third one for 3 seconds.
Also if one of the snackbars gets closed, the second one in the "queue" should be displayed and so on. What I'm doing at the moment is dispatching the action that displays the message in Epics that catch the errors, then I'm creating Epics dispatch the action to close the snackbar after 3 seconds of delay, with the action CLOSE_SNACKBAR. Which methods should I use in order to achieve this?
A basic Epic I've implemented looks like this, basically the action that changes the state from the snackbar to open and displays the error message is dispatched from another epic, the one that catches the error, and then another Epic dispatches the action to close the snackbar after 3 seconds, but I haven't figured out how to do this queueing kind of action so that each message can be displayed for 3 seconds each, right now if an error occurs right after the first one, the second message will be displayed without waiting the 3 seconds after the previous message.
Here are some basic examples of my epics (Ignore de request part, ):
const getUserEpic = (action$, store) => (
action$.ofType(actionTypes.DATA_REQUESTED)
.switchMap(action => {
const { orderData, queryData, pagerData } = store.getState().usuario.list;
const params = constructParams(queryData, pagerData, orderData);
return Observable.defer(() => axios.get(`users`, { params }))
.retry(NETWORK.RETRIES).mergeMap(response => {
Observable.of(actions.dataRequestSucceeded(response.data.rows))
}).catch(error => Observable.concat(
Observable.of(actions.dataRequestFailed(error)),
Observable.of({
type:'DISPLAY_DATA_REQUEST_FAILED_MESSAGE',
open:true,
message:'Failed to get Users Data'
})
));
})
)
const getRoleEpic = (action$, store) => (
action$.ofType(actionTypes.DATA_REQUESTED)
.switchMap(action => {
const { orderData, queryData, pagerData } = store.getState().usuario.list;
const params = constructParams(queryData, pagerData, orderData);
return Observable.defer(() => axios.get(`role`, { params }))
.retry(NETWORK.RETRIES).mergeMap(response => {
Observable.of(actions.dataRequestSucceeded(response.data.rows))
}).catch(error => Observable.concat(
Observable.of(actions.dataRequestFailed(error)),
Observable.of({
type:'DISPLAY_DATA_REQUEST_FAILED_MESSAGE',
open:true,
message:'Failed to get Roles Data'
})
));
})
)
This two epics do basically the seme, they are doing a GET request to the backend, but if they fail, they dispatch the action that opens the snackbar with an error message, and both error messages are different.
And this one is the epic that currently closes the Snackbar after 3 seconds:
const displayDataRequestFailedEpic = (action$, store) => (
action$.ofType(actionTypes.DISPLAY_DATA_REQUEST_FAILED)
.debounceTime(3e3)
.switchMap( action => {
return Observable.of({
type:'CLOSE_SNACKBAR',
open:false,
message:''
})
})
)
Imagine I do both requests really fast and both of them fail. I want show all the errors that happened, one after another for 3 seconds each,
This hasn't been tested...
First, need to separate the action that displays messages from the action that is causing the display:
const getUserEpic = (action$, store) => (
action$.ofType(actionTypes.DATA_REQUESTED)
.switchMap(action => {
const { orderData, queryData, pagerData } = store.getState().usuario.list;
const params = constructParams(queryData, pagerData, orderData);
return Observable.defer(() => axios.get(`users`, { params }))
.retry(NETWORK.RETRIES).mergeMap(response => {
return Observable.of(actions.dataRequestSucceeded(response.data.rows))
}).catch(error => Observable.of(actions.dateRequestFailed(error))
// }).catch(error => Observable.concat(
// Observable.of(actions.dataRequestFailed(error)),
//
// this will be handled elsewhere
//
// Observable.of({
// type:'DISPLAY_DATA_REQUEST_FAILED_MESSAGE',
// open:true,
// message:'Failed to get Users Data'
// })
));
})
)
next, need to define some method that creates the DISPLAY_MESSAGE action from whatever action is triggering the message. Included in the action should be a unique id used to track which message needs to be closed.
Finally, use concatMap() to create and destroy the messages.
const displayEpic = action$ => action$
.ofType(actionTypes.DATA_REQUEST_FAILED)
.concatMap(action => {
const messageAction = actions.displayMessage(action);
const { id } = messageAction;
return Rx.Observable.merge(
Rx.Observable.of(messageAction),
Rx.Observable.of(actions.closeMessage(id))
.delay(3000)
.takeUntil(action$
.ofType(actionTypes.CLOSE_MESSAGE)
.filter(a => a.id === id))
);
});

Resources