Why my effect is running serveral times after action is called? - javascript

I have this effect that request serveral values to retrive a product from service. Afer dispatch REQUEST_PRODUCTS is called one time as expected, but when I tried go to other location in the routing the this.apiMarketServices is called serveral times, this trigger the router navigate and this will redirect to previous page. The action REQUEST_PRODUCTS is dispatched one time. Why this effect is called serveral times?
Do I need add some kind of stop to the effect in order to avoid the called after the return GetSuccess and GetFailed?
#Effect()
requestProductsFromMarket = this.actions$
.ofType(REQUEST_PRODUCTS)
.withLatestFrom(this.store)
.switchMap(([action, store]) => {
const id = store.product.id;
return this.savedProducts.getProduct(id, 'store');
})
.switchMap(_ => this.stateService.getMarketId())
.switchMap(({ marketId }) =>
this.apiMarketServices.get(MARKETS_PROFILES + marketId)
)
.withLatestFrom(this.store)
.map(([r, store]) => {
const ser = r.data.map(s => s.legId);
const storSer =
store.product.serIds;
if (storSer.every(s =>ser.includes(s))) {
this.router.navigate([
`/products/edit/${store.products.id}`
]);
return GetSuccess;
} else {
return GetFailed;
}
})
.catch(() => of(GetQueryFailed));

The solution for defect is related to an Observable. In the debugging the "this.apiMarketServices.get(MARKETS_PROFILES + marketId)" was called several times, I realted this services like cause of the defect:
.switchMap(({ marketId }) =>
this.apiMarketServices.get(MARKETS_PROFILES + marketId)
)
But the real cause was the stateSevice, this behavior subject was updated with next, in anothers parts of the app.
.switchMap(_ => this.stateService.getMarketId())
In order to avoid those calls, I created a function in order to retrive the current value from the BehaviorSubject.
getCurrentMarketId(): ClientData {
return this.currentMarket.value; // BehaviorSubject
}
I added this function to the effect the call is once per dispatched effect.
...
.switchMap(([action, store]) => {
const id = store.product.id;
return this.savedProducts.getProduct(id, 'store');
})
.map(_ => this.stateService.getCurrentMarketId())
.switchMap(({ marketId }) =>
this.apiMarketServices.get(MARKETS_PROFILES + marketId)
)

Related

Subscribe to observable, async map result with input from dialog, use result from map to route

I am calling an API-service which returns an Observable - containing an array of elements.
apiMethod(input: Input): Observable<ResultElement[]>
From this I have been choosing the first element of the array, subscribing to that. Then used that element to route to another page like this:
this.apiService
.apiMethod(input)
.pipe(map((results) => results[0])
.subscribe(
(result) => {
return this.router.navigate('elements/', result.id)
}
)
This works just fine.
Problem is, I do not want to just use the first element, I want a MatDialog, or other similar to pop up, and give the user option of which element to choose, and THEN route to the correct one.
If the list only contain one element though, the dialog should not show, and the user should be routed immediately.
I have tried to open a dialog in the .pipe(map()) function, but the subscribe() happens before I get answer from the user, causing it to fail. And I am not sure if that even is the correct approach. How would any of you solve this problem?
Edit
Ended up doing partly what #BizzyBob suggested:
Changing map to switchmap in the API-call, making it this way:
this.apiService
.apiMethod(input)
.pipe(switchMap((results) => this.mapToSingle(results)
.subscribe(
(result) => {
return this.router.navigate('elements/', result.id)
}
)
With the mapToSingle(ResultElement[]) being like this:
private mapToSingle(results: ResultElement[]): Observable<ResultElement> {
if (result.length === 1){
return of(results[0]);
}
const dialogConfig = new MatDialogConfig<ResultElement[]>();
dialogConfig.data = results;
const dialogRef = this.dialog.open(ResultDialogComponent, dialogConfig);
return dialogRef.afterClosed();
}
I would create a DialogComponent that takes in the list of choices as an input, and emits the chosen item when it's closed.
Then, create a helper method (maybe call it promptUser) that simply returns an observable that emits the selected value:
this.apiService.apiMethod(input)
.pipe(
switchMap(results => results.length > 1
? this.promptUser(results)
: of(results[0])
)
)
.subscribe(
result => this.router.navigate('elements/', result.id)
);
Here we simply use switchMap to return an observable that emits the proper item. If the length is greater than 1, we return the helper method that displays the dialog and emits the chosen item, else just emit the first (only) item. Notice that we wrapped plain value with of since within switchMap, we need to return observable.
In either case, the desired item is emitted and received by your subscribe callback.
Two possible options:
Having a subject for the selected result that is "nexted" either by user input or a side effect of getting an api result with one element.
Keeping track of an overall state of the component and responding appropriately whenever a selectedResult is set in the state.
The example below is a sketch of using an Observable to keep track of the component's state.
There are two input streams into the state, the results from the api and the user input for the selected result.
Each stream is converted into a reducer function that will modify the overall state.
The UI should subscribe to this state via an async pipe, showing the modal when appropriate, and updating updating state from events via the Subjects.
The redirection should come as an effect to the change of the state when selectedResult has a value.
readonly getResultsSubject = new Subject<MyInput>();
readonly resultSelectedSubject = new Subject<ResultType>();
private readonly apiResults$ = this.getResultsSubjects.pipe(
switchMap((input) => this.apiMethod(input))
);
readonly state = combineLatest([
this.apiResults$.pipe(map(results => (s) => results.length === 1
? { ...s, results, selectedResult: x[0], showModal: false }
: { ...s, results, showModal: results.length > 1 })),
this.resultSelectedSubject.pipe(map(selectedResult => (s) => ({ ...s, selectedResult })))
]).pipe(
scan((s, reducer) => reducer(s), { }),
shareReplay(1)
);
ngOnInit() {
this.state.pipe(
filter(x => !!x.selectedResult)
).subscribe(x => this.router.navigate('elements/', x.selectedResult.id));
}
I've been using this pattern a lot lately. It makes it pretty easy the number of actions and properties of the state grow.
I would solve it using the following method:
Get the data with your subscribe (without the pipe). And save this data in the component variable
options: any;
this.apiService
.apiMethod(input)
.subscribe(
(result) => {
if (result.length === 1) {
this.router.navigate([result[0]]);
return;
}
options = result;
}
)
with an ngIf on the modal (conditional of the length of the array of options > 0 display the component with the different choices when the data is received
<modal-component *ngIf="options.length > 0"></modal-component>
when the user (click) on an option inside your modal, use the router to redirect.
html
<div (click)="redirect(value)">option 1</div>
ts
redirect(value) {
this.router.navigate([value]);
}
That would be the most straight forward

Web-Bluetooth API, can not update characteristics. time-dependent updating possible?

I try to get the characteristics everytime they change, the problem is, they change but the eventListener doesn't recognize it. So i only get the first value, after connecting to my BLE, but nothing happen after that. Is there something wrong in my code ?
Another Question, is there a way to update the characteristic every, i dont know 5 Seconds for example? And how would you do that, are there any code examples?(Maybe with setInterval() ? )
Thank you !
function test() {
console.log('Requesting Bluetooth Device...');
navigator.bluetooth.requestDevice({
acceptAllDevices: true,
optionalServices: ['af07ecb8-e525-f189-9043-0f9c532a02c7']
}) //c7022a53-9c0f-4390-89f1-25e5b8ec07af
.then(device => {
console.log('Gatt Server Verbindung');
return device.gatt.connect();
})
.then(server => {
console.log('Dose Service...');
return server.getPrimaryService('af07ecb8-e525-f189-9043-0f9c532a02c7');
})
.then(service => {
console.log('mgyh Characteristic...');
return service.getCharacteristic('a99e0be6-f705-f59c-f248-230f7d55c3c1');
})
.then(characteristic => {
// Set up event listener for when characteristic value changes.
characteristic.addEventListener('characteristicvaluechanged',dosechanged);
return characteristic.readValue();
})
.catch(error => {
console.log('Das geht nicht: ' + error);
});
}
function dosechanged(event) {
let dose = event.target.value.getUint8(0)+event.target.value.getUint8(1)*10+event.target.value.getUint8(2)*100 + event.target.value.getUint8(3)*1000+ event.target.value.getUint8(4)*10000;
console.log('Received ' + dose);
}
You missed a characteristic.startNotifications() call to start receive notification. example
setInterval would be fine to call readValue() every 5 seconds.

Test not looping in protractor

I have a test that I'm trying to fix. It needs to click on each result and verify the price and name match from the initial result with the final individual result. Currently my code will click on only the first image but will not navigate back to the results page to try the next result. I tried to remove the first() as my understanding is that method only takes the very first element and ignores the rest. Sadly that didn't work. What am I missing?
tester.it('links to the correct product details page when a result is clicked', () => {
const $offer = element.all(by.css('#main-results .catalog-offer')).first();
const offerResultsText = [];
let offerResultsPrice;
return Promise.all([
$offer.all(by.css('.offer-name .name-part')).map(($namePart) =>
$namePart.getText().then((partText) => offerResultsText.push(partText))
),
$offer
.element(by.css('.price'))
.getText()
.then((offerPrice) => (offerResultsPrice = offerPrice)),
])
.then($offer.element(by.tagName('a')).click)
.then(() =>
browser.wait(
protractor.ExpectedConditions.presenceOf(
element(by.css('#recently-purchased-details'))
),
5000
)
)
.then(() =>
expect(element(by.css('.details .subtotal > span')).getText()).to.eventually.equal(
offerResultsPrice
)
)
.then(() => {
return offerResultsText.map((sourceString) => {
console.log(sourceString);
return expect(
element(by.css('.details .setting .info')).getText()
).to.eventually.contains(sourceString);
});
});
});
Figured out what I was doing wrong. I needed to remove the return and then use our internal method afterward to loop through the results urls. Looked like this in the end...
expect(testerUtils.getPageId()).to.eventually.equal('Recently Purchased Engagement Ring Details');
testerUtils.go(testContext.url);

RxJs - What is the proper pattern/way to create an Observable array of Observables?

Am fairly new to RxJs, and trying to wrap my head around what the proper pattern is to simply create an Observable array of Observables.
I want to retrieve a list of User's Posts. The Posts themselves should be Observables, and I want to keep them all in an Observable array, so that when the array changes the calling code should be notified and update anything subscribed to the post "list". This is simple enough, but I also would like each of the Posts to be Observables, so if I retrieve a specific posts[i] from it, I should also be able to subscribe to these individual objects.
What is the proper way to do this?
Am using Angular 9, I have:
public getPosts(): Observable<Array<Post>> {
return new Promise((resolve, reject) = {
let posts: Observable<Array<Post>> = new Observable<Array<Post>>();
this.get<Array<Post>>('posts').subscribe(r => {
posts = from(r);
return resolve(posts);
});
});
}
This gives me an Observable<Array<Post>>, but how should I create an Observable<Array<Observable<Post>>>?
Is this an anti-pattern?
It all comes to convenience, if your server serves you differential data of what changed in post, then go ahead and create Observable<Observable<Post>[]>.
In your post, however, there are multiple problems. You cannot mix Observables with Promises. The method getPosts will return only the first post you get from API.
This is the solution ask for, but I am not sure, it is what you actually wanted...
public getPosts(): Observable<Array<Observable<Post>>> {
return this.get('posts').pipe(
switchMap(posts => combineLatest(
posts.map(post => this.get('post', post.id))
)),
);
}
it's unclear what you're trying to accomplish here, but you might want something more like this:
#Injectable({providedIn:'root'})
export class PostService {
// private replay subject will cache one value
private postSource = new ReplaySubject<Post[]>(1)
// public list of posts observable
posts$ = this.postSource.asObservable();
// function to select item by id out of list
post$ = (id) => this.posts$.pipe(map(posts => posts.find(p => p.id === id)))
getPosts() {
// function to get remote posts
return this.get<Post[]>('posts');
}
loadPosts() {
// function to load posts and set the subject value
this.getPosts().subscribe(posts => this.postSource.next(posts));
}
}
you'll have to define that get function and call loadPosts everytime you want to update the list.
Given informations:
!If any of this statements is wrong, please tell me and I will update the answer!
get function that returns an observable with one array that filled with posts
the get observable emits always when the posts are changing
the value inside the observable (Array>) is no observable and does not change over time
this.get<Array<Post>>('posts')
Possible functions
() => getPostById$
// This function returns you an observable with the post related to your id.
// If the id is not found the observable will not emit
// If the id is found the observable will only emit if the interface values have been changed
function getPostById$(id: string): Observable<Post> {
// Returns you either the post or undefined if not found
const findId = (id: string) => (posts: Array<Post>): Post | undefined =>
posts.find(post => post.id === id);
// Allows you only to emit, if id has been found
const existingPost = (post: Post | undefined): boolean => post != null;
// Allows you only to emit if your id has been changed
const postComparator = (prevPost: Post, currPost: Post): boolean =>
prevPost.value === currPost.value && prevPost.name === currPost.name;
return this.get('posts').pipe(
map(findId(id)),
filter(existingPost),
distinctUntilChanged(postComparator)
);
}
() => getPosts$
function getPosts$(): Observable<Array<Post>> {
return this.get('posts');
}
() => getStatePosts$
// This function allows to manage your own state
// 1. posts$: overwrites all posts
// 2. clear$: empties your posts$ observable
// 3. add$: adds one observable to the end of your posts
function statePosts$(posts$: Observable<Array<Posts>>, clear$: Observable<void>, add$: Observable<Post>): Observable<Array<Post>> {
const updatePosts = (newPosts: Array<Posts>) => (oldPosts: Array<Posts>) => newPosts;
const clearPosts = () => (oldPosts: Array<Posts>) => [];
const addPost = (post: Post) => (oldPosts: Array<Posts>) => [...oldPosts, post];
return merge(
// You can add as much update functions as you need/want (eg: deleteId, addPostAtStart, sortPosts, ...)
posts$.pipe(map(updatePosts)),
clear$.pipe(map(clearPosts)),
add$.pipe(map(addPost))
).pipe(
// The fn in you scan is the (oldPosts: Array<Posts>) function from one of your three update functions (updatePosts, clearPosts and addPosts).
// Whenever one of those three observables emits it first calls the left side of the function inside the map (post: Post) and returns a new function
// When this function reaches the scan it gets the oldPosts and is able to update it
scan((oldPosts, fn) => fn(oldPosts), [])
)
}
// Usage
private posts$: Observable<Array<Post>> = this.get('posts');
private clear$: Subject<void> = new Subject();
private add$: Subject<Post> = new Subject();
public statePosts$ = getStatePosts(posts$, clear$, add$);
Hint: Try to read the functions from the return statement first. And then check what is happening in the mapping/filtering or other operations. Hopefully I did not confuse you too much. If you have questions, feel free to ask.

Is there a better solution of this RxJS epic stream?

I have a redux state using redux-observable's epics.
I need to solve showing a message after a user deletes an object or more objects.
There are two ways how to delete an object:
by action deleteObject(id: string) which call deleteObjectFulfilled action
by action deleteObjects(ids: Array<string>) which call N * deleteObject(id: string) actions
I want to show only one message with a count of deleted messages after every success "deleting action".
My final solution of this epic is:
export const showDeleteInformationEpic = action$ =>
combineLatest(
action$.pipe(ofType(DELETE_OBJECT_FULFILLED)),
action$.pipe(
ofType(DELETE_OBJECTS),
switchMap(({ meta: { ids } }) =>
action$.pipe(
ofType(DELETE_OBJECT_FULFILLED),
skip(ids.length - 1),
map(() => ids.length),
startWith('BATCH_IN_PROGRESS'),
take(2),
),
),
startWith(1),
),
).pipe(
startWith([null, null]),
pairwise(),
map(([[, previousCount], [, currentCount]]) =>
(previousCount === 'BATCH_IN_PROGRESS')
? currentCount
: isNumber(currentCount) ? 1 : currentCount),
filter(isNumber),
map((count) => throwInfo('objectDeleted', { count })),
);
Can you see any better solution of this?
There is more simple solution if I use only deleteObjects(Array<string>) for both cases..
Instead of firing multiple actions, you can create and dispatch a single action DELETE_MULTIPLE and pass all the id(s) in the payload.
This way your effects will be a lot cleaner since you only have to subscribe to DELETE_MANY action and additionally, it will prevent multiple store dispatches.

Categories