Reputation: 41
Really need some help here figuring out this conundrum I'm facing.. I'm gonna paint it in terms of production of donuts. (that circle thing we eat when we're stressed out)
How it should be
In an ideal case, user should be able to change Factory & Topping settings of a donut.
The case is, every time a donut is imported, the selected factory will make a donut with selected topping immediately.
The duration of making a donut varies.
After a donut is made, it will be stored.
Now, the problem I am facing is that I am unable to change Factory & Topping. (It is always the default value N1
, playing with the codes below should help explain the conundrum better.)
Note
Sequence matters. If user keys in
a
,b
,c
, the donuts should be["N1a", "N1b", "N1c"]
, even if donut a particular type of donut takes a longer time to make. If user keys ina
,2
,i
,j
,k
,3
,x
,y
, the donuts should be["N1a", "N2i", "N2j", "N2k", "N3x", "N3y"]
Here's the code I've worked out below, hope you rxjs experts out there find joy in solving this as I've been eating too much donuts trying to solve this already!
Code: https://stackblitz.com/edit/rxjs-uhqftu
import {
fromEvent, combineLatest, of, concat, merge,
} from 'rxjs';
import {
filter, tap, map, concatMap,
} from 'rxjs/operators';
console.log( 'Press arrow keys to change factory' );
console.log( 'Press [0-9] to change topping' );
console.log( 'Press [a-z] to import donuts' );
const donuts = [];
const allEvents$ = concat( of( 'ArrowUp', '1' ), fromEvent( document, 'keydown' ).pipe( map( event => event.key ) ) );
const factoryLocations = {
ArrowUp: 'N',
ArrowLeft: 'W',
ArrowDown: 'S',
ArrowRight: 'E',
};
const factoryEvent$ = allEvents$.pipe(
filter( val => factoryLocations[ val ] ),
map( val => factoryLocations[ val ] ),
tap( x => console.log( `%c Change Factory ${ x }`, 'color: #e57373' ) )
);
const toppingEvent$ = allEvents$.pipe(
filter( val => Number.isInteger( Number( val ) ) ),
tap( x => console.log( `%c Change Topping ${ x }`, 'color: #f06292' ) )
);
const importDonutEvent$ = allEvents$.pipe(
filter( val => !Number.isInteger( Number( val ) ) && !factoryLocations[ val ] ),
tap( x => console.log( `%c Importing Donut ${ x }`, 'color: #f06292' ) )
);
const settingEvents$ = combineLatest( factoryEvent$, toppingEvent$ );
const processEvent$ = settingEvents$.pipe(
concatMap( ( [ factory, topping ] ) => merge(
importDonutEvent$
).pipe(
map( type => ( { factory, topping, type } ) ),
) )
);
processEvent$.pipe(
concatMap( ( { factory, topping, type } ) => makeDonut( factory, topping, type ) ),
).subscribe( donut => {
console.log( `%c DONE ${ donut }`, 'color: #4db6ac' );
donuts.push( donut );
console.log( donuts );
} );
function makeDonut ( factory, topping, type ) {
const time = Math.random() * 3000;
const donut = factory + topping + type;
console.log( `%c MAKING ${ donut }`, 'color: #7986cb' );
return new Promise( resolve => setTimeout( () => resolve( donut ), time ) );
}
Upvotes: 4
Views: 118
Reputation: 1960
The other answer is correct. I'd just like to add it's because the concatMap
waits for the inner observable to complete before it runs its function to produce a new inner observable. In your case importDonutEvent$
never completes, so the values emitted by settingEvents$
are queued indefinitely.
Your demo is good, but have you considered using marble tests to highlight the issue? It's much easier to test and document behaviour. I've created a stackblitz showing the issue, with two passing implementations and one failing. The second alternative shown is the use of the operator withLatestFrom
.
Upvotes: 0
Reputation: 6070
I am a bit late to the party, but the solution is to use switchMap()
instead of concatMap()
in processEvent$
. Here is the code:
const processEvent$ = settingEvents$.pipe(
switchMap( ( [ factory, topping ] ) => // change to switchMap and delete redundant merge
importDonutEvent$.pipe(
map( type => ( { factory, topping, type } ) ),
)
)
);
And here is a working StackBlitz
Upvotes: 1