Collectives™ on Stack Overflow

Find centralized, trusted content and collaborate around the technologies you use most.

Learn more about Collectives

Teams

Q&A for work

Connect and share knowledge within a single location that is structured and easy to search.

Learn more about Teams

I have two separate but overlapping user interaction flows that both trigger the same action. If one flow is started, the other one should not start until the first is complete. I'm probably overthinking this but I can't find a pattern that does what I want.

Concrete example:

I want to open a context menu for a selection in a text area with two separate interaction flows:

  • Via Mouse: Mouse Down -> Selection Changed -> Mouse Up
  • Via Keyboard: Selection Changed -> Timeout(500ms)
  • As you can see, both flows overlap in the "Selection Changed" part, but the first and final conditions are different. When selecting with the keyboard, there is no definite end condition, so I rely on a timer. When selecting with the mouse there is a definite end condition (Mouse Up), so I want to show the menu immediately. Both are defined as separate Observables that complete on their end condition.

    To differentiate between the two, I want to wait for whichever observable emits first and then only listen to that observable until it completes and then start again. So when there is a Mouse Down event I want to wait for the Mouse Up event to trigger. When there is a selection change without the Mouse Down, I start the timeout.

    Things I tried:

    exhaust/exhaustMap: That is technically what I want (follow one Observable to the end before considering the next) but those don't take a list of Observables but a function that emits observables. My observables are fixed though. I don't know how to wire this up to do what I want.

    race: I can pass in a list of observables and whatever emits first is then followed through until the end. However, it won't resubscribe when it completes. race() simply completes and that's that. I tried something like this:

    const openMenu$ = of(true).pipe(
      startWith(race(a$, b$)),
      exhaust(),
      repeat()
    
    const openMenu$ = of(true).pipe(
      exhaustMap(() => race(a$, b$)),
      exhaust(),
      repeat()
    
    const openMenu$ = race(a$, b$).pipe(
      repeat()
    

    but all of these just repeat the first Observable over and over. I found no way to restart the race.

    Any ideas? Or am I going at this completely wrong?

    Why isn't the 3rd approach working? if a$ emits first, b$ will be unsubscribed and then when a$ finally completes, repeat should be reached, which should resubscribe to a$ and b$. – Andrei Gătej Jul 27, 2020 at 14:51

    A simple race should be repeatable.

    import { defer, timer, race } from 'rxjs';
    import { map } from 'rxjs/operators';
    // an observable that will emit `0` after a random time less than 3 seconds
    const randomTime$ = defer(() => timer(Math.random() * 3000));
    // Will randomly emit "a" after a random time less than 3 seconds
    const a$ = randomTime$.pipe(map(() => 'a'));
    // Will randomly emit "b" after a random time less than 3 seconds
    const b$ = randomTime$.pipe(map(() => 'b'));
    // Race a$ and b$
    const source$ = race(a$, b$).pipe(
      // repeat the race 5 times.
      random(5)
    source$.subscribe(console.log);
    

    The startWith and exhaust are unnecessary in the code you have above. race(a$, b$).pipe(repeat()) should be all you need.

    I would check that the first observable isn't always emitting before the second one. Perhaps it has it's own startWith or some sort of synchronous emission that guarantees it "wins" the race. If you race two synchronous observables, the first one always wins.

    You were right, I had a problem with my Observables and the pipe after the race. The Observables would never complete (this is by design) but the pipe after the race would also never complete (since the sources don't complete) and therefore never unsubscribe and also never trigger the repeat, thus never resubscribing. After adding a take(1) in the right place (especially before all the filtering) everything works now just as expected. Thinking in streams is still unfamiliar for me... – MadMonkey Jul 28, 2020 at 9:18

    Thanks for contributing an answer to Stack Overflow!

    • Please be sure to answer the question. Provide details and share your research!

    But avoid

    • Asking for help, clarification, or responding to other answers.
    • Making statements based on opinion; back them up with references or personal experience.

    To learn more, see our tips on writing great answers.