How to make rxjs non ui blocking? - rxjs

I have an array of items. Each item in that array represents a row in my html table and is automatically rendered when the array of items is changing.
I now wanted to use rxjs to add some items to that table when I click a button, so I did the following:
Rx.Observable.fromEvent(this.$.button, 'click')
.switchMap(() => Rx.Observable.range(0, 10000))
.subscribe((x) => {
this.push('items', {id: '' + x, description: '' + x});
});
But this is freezing the ui until every element is pushed into the array.
How can I implement it so that the ui is not freezing and still responses to user inputs?

If you don't want range to synchronously emit the entire range of values, you can specify a scheduler.
For example:
Rx.Observable
.fromEvent(this.$.button, 'click')
.switchMap(() => Rx.Observable.range(0, 10000, Rx.Scheduler.asap))
.subscribe((x) => {
this.push('items', {id: '' + x, description: '' + x});
});

Related

call two different methods based on emissions of two observables

currently, I have a cabinet with some drawers, each drawer has some items that users can manipulate. Whenever the user switches to a different drawer an observable (D) fires. As soon as the user swithces to a drawer, another observable (I) fires with an array of Items available in that drawer.
My desired behaviour is for the follwoing stream:
Drawer(D): --D----------------------------D--...
Items(I): -------I----I-----I-----I-----I----I--..
for an emission (D) and first following emission (I) => call a function (setCurrentDrawerItems())
for all following emissions of (I) that are not interrupted by an emission (D) =>
call the function (userPickedItemFromDrawer())
how can I achieve this using rxjs operators?
One approach is to tag your observable emissions to cleanly separate the two in the merged stream. For example, shoving them into an object with the label t. Then you can just query that label to know what to do next.
Something like this might work:
merge(
drawer$.pipe(map(v => ({ t: 'D', v }))),
items$.pipe(map(v => ({ t: 'I', v })))
).pipe(
startWith({ t: 'I', v: null }),
pairwise()
).subscribe(([prev, curr]) => {
if (curr.t == 'D' || (curr.t == 'I' && prev.t == 'D')) {
setCurrentDrawerItems(curr.v);
} else {
userPickedItemFromDrawer(curr.v);
}
});

Cypress how to assert that console was calledWith array of x length?

I'm trying to find how to assert that the console log was called with an array of a defined fixed length and I'm not sure how to do that. I'm not very familiar with Mocha/Sinon, which Cypress seems to be using.
To give a bit more of a background of my App does, it's a Queued Editing, I change a few cell and they change color, once I'm ready to click save then it shows an array of all the cells that got changed (in my particular test, 11 got changed). I know how to test the entire calledWith comparison but I just want to test the array length and nothing more (since I have random data, I can't test an equal match of data but the array length will be sufficient).
So far I have this code that works for the most part except for the array length
describe('My Sample', () => {
beforeEach(() => {
// create a console.log spy for later use
cy.window().then((win) => {
cy.spy(win.console, 'log');
});
});
it('should click on the "Save" button and expect 2 console log calls with the queue items', () => {
cy.get('[data-test=save-all-btn]').click();
cy.get('.unsaved-editable-field')
.should('have.length', 0);
cy.window().then((win) => {
expect(win.console.log).to.have.callCount(2);
expect(win.console.log).to.be.calledWith(Array[11]); // how to test array length of 11?
});
});
Also, maybe as a second question, how could we assert that this array contains a certain object property with x value? In Jest we can do it this way. I just haven't seen anything similar in Cypress, is that kind of test possible in Cypress?
// for an array
const oddArray = [1, 3, 5, 7, 9, 11, 13];
test('should start correctly', () => {
expect(oddArray).toEqual(expect.arrayContaining([1, 3, 5, 7, 9]));
});
// for an array of objects
const users = [{id: 1, name: 'Hugo'}, {id: 2, name: 'Francesco'}];
test('we should have ids 1 and 2', () => {
expect(users).toEqual(
expect.arrayContaining([
expect.objectContaining({id: 1}),
expect.objectContaining({id: 2})
])
);
});

How to preserve a 'complete' event across two RxJS observables?

I have an observable const numbers = from([1,2,3]) which will emit 1, 2, 3, then complete.
I need to map this to another observable e.g. like this:
const mapped = numbers.pipe(
concatMap(number => Observable.create(observer => {
observer.next(number);
}))
);
But now the resulting observable mapped emits 1, 2, 3 but not the complete event.
How can I preserve the complete event in mapped?
Your code gives me just "1" (with RxJS 6); are you sure you see 3 values?
Rx.from([1,2,3]).pipe(
op.concatMap(number => Rx.Observable.create(observer => {
observer.next(number);
}))
).forEach(x => console.log(x)).then(() => console.log('done'))
You're never completing the created Observable (it emits one value but never calls observer.complete()). This works:
Rx.from([1,2,3]).pipe(
op.concatMap(number => Rx.Observable.create(observer => {
observer.next(number); observer.complete();
}))
).forEach(x => console.log(x)).then(() => console.log('done'))
This all shows how hard it is to use Rx.Observable.create() correctly. The point of using Rx is to write your code using higher-level abstractions. A large part of this is preferring to use operators in preference to observers. E.g. in your case (which is admittedly simple):
Rx.from([1,2,3])
.pipe(op.concatMap(number => Rx.of(number)))
.forEach(x => console.log(x)).then(() => console.log('done'))

Conditionally producing multiple values based on item value and merging it into the original stream

I have a scenario where I need to make a request to an endpoint, and then based on the return I need to either produce multiple items or just pass an item through (specifically I am using redux-observable and trying to produce multiple actions based on an api return if it matters).
I have a simplified example below but it doesn't feel like idiomatic rx and just feels weird. In the example if the value is even I want to produce two items, but if odd, just pass the value through. What is the "right" way to achieve this?
test('url and response can be flatMap-ed into multiple objects based on array response and their values', async () => {
const fakeUrl = 'url';
axios.request.mockImplementationOnce(() => Promise.resolve({ data: [0, 1, 2] }));
const operation$ = of(fakeUrl).pipe(
mergeMap(url => request(url)),
mergeMap(resp => resp.data),
mergeMap(i =>
merge(
of(i).pipe(map(num => `number was ${num}`)),
of(i).pipe(
filter(num => num % 2 === 0),
map(() => `number was even`)
)
)
)
);
const result = await operation$.pipe(toArray()).toPromise();
expect(result).toHaveLength(5);
expect(axios.request).toHaveBeenCalledTimes(1);
});
Personally I'd do it in a very similar way. You just don't need to be using the inner merge for both cases:
...
mergeMap(i => {
const source = of(`number was ${i}`);
return i % 2 === 0 ? merge(source, of(`number was even`)) : source;
})
I'm using concat to append a value after source Observable completes. Btw, in future RxJS versions there'll be endWith operator that will make it more obvious. https://github.com/ReactiveX/rxjs/pull/3679
Try to use such combo - partition + merge.
Here is an example (just a scratch)
const target$ = Observable.of('single value');
const [streamOne$, streamTwo$] = target$.partition((v) => v === 'single value');
// some actions with your streams - mapping/filtering etc.
const result$ = Observable.merge(streamOne$, streamTwo$)';

RxJs. Combining latest and once

I have a UI like this:
Where I can
Enter something in search box
Drag users from table to chart
The logic required is:
Initially chart shows some subset of all users (e.g., first 10)
When users are dragged on chart they are added to the users that already there
When filter is applied all users are removed from chart and then it is repopulated with some subset of matching users
I am trying to implement such logic with RxJs.
I have filteredUsers$ and addedUsers$ stream that produce users matching filter and dragged users correspondingly.
I need to combine them in such way:
Observable
.<OPERATOR>(filteredUsers$, addedUsers$)
.subscribe(([filteredUsers, addedUsers]) => {
// When filteredUsers$ fires:
// filteredUsers is value from stream
// addedUsers == null
// When addedUsers$ fires:
// filteredUsers is latest available value
// addedUsers is value from stream
redrawChart(/* combining users */)
});
Any ideas how I can achieve this?
Time sequence:
Filtered: - a - - - - a - ->
Added : - - b - b - - - ->
Result : - a ab - ab - a - ->
If you want the final stream to be populated only when addUsers$ fires with latest from could be a solution:
So, in your case addUsers$ could be the first stream.
You can try out the following code:
let firstObservable$ = Observable.from([1, 2, 3, 4])
.zip(Observable.interval(50), (a, b) => {
return a;
});
let secondObservable$ = Observable.from([5, 6])
.zip(
Observable.interval(70), (a, b) => {
return a;
});
firstObservable$
.withLatestFrom(secondObservable$, (f, s) => ({ a: f, b: s }))
.subscribe(x => {
console.log('result: ', x);
});
The first observable emits every 50 ms a value from the array.
The second observable every 75 ms.
The values printed are {a: 2, b: 5} {a: 3, b: 6} {a: 4, b: 6}
Because 1 was emitted before 5 we lose the pair (1,5)!
I am not clear but missing a pair from addUsers$ if the other stream has not emitted may be non-desired behavior for you.
You could overcome that if you start the second stream with an initial value and then filter out any results you don't want.
You have the combineLatest operator which basically does what you are describing. It combines two observables and gives you the latest value of both streams.
So:
--a--b-----c---
-x-----d-----p-
-combineLatest-
--a--b-b---c-c
x x d d p
This should allow you to do what you want if I understand correctly.
Here's the official doc link:
https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/combinelatest.md
Eventually I have done it by adding additional subject:
var filteredUsers$ = ... // users from filter
var addedUsers$ = ... // users dragged on chart
var usersToDraw$ = new Subject();
subscriptions:
usersToDraw$
.subscribe(usersToDraw => {
redrawChart(usersToDraw);
});
filteredUsers$
.subscribe(filteredUsers => {
usersToDraw$.next(filteredUsers);
});
Observable
.combineLatest(filteredUsers$, addedUsers$)
.filter(([filteredUsers, addedUsers]) => addedUsers != null)
.subscribe(([filteredUsers, addedUsers]) => {
// we 'clear' stream so the same users won't be added twice
addedUsers$.next(null);
usersToDraw$.next(/* combining users */);
});
UPDATE
The solution can be improved with withLatestFrom (thanks #nova)
usersToDraw$
.subscribe(usersToDraw => {
redrawChart(usersToDraw);
});
filteredUsers$
.subscribe(filteredUsers => {
usersToDraw$.next(filteredUsers);
});
addedUsers$
.withLatestFrom(filteredUsers$)
.subscribe(([addedUsers, filteredUsers]) => {
usersToDraw$.next(/* combining users */);
});

Resources