linchenhsin
linchenhsin

Reputation: 41

Need rxjs expert to solve this blardy conundrum :)

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 in a, 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

Answers (2)

NickL
NickL

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

dmcgrandle
dmcgrandle

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

Related Questions