Pause RxJS stream based on a value in the stream

前端 未结 6 440
一生所求
一生所求 2021-01-03 23:56

I have a simple component with a single button that starts and pauses a stream of numbers generated by RxJS timer.

import { Component, OnInit } from \'@angul         


        
6条回答
  •  北荒
    北荒 (楼主)
    2021-01-04 00:17

    It's possible to either (1) expand your current bufferToggle / windowToggle approach or to (2) use a custom buffer implementation.

    1. Expanding the bufferToggle / windowToggle approach

    You can add an array to the operator queue after bufferToggle.

    1. When bufferToggle emits append those values to the array.
    2. Take values from the array until a certain element in the array matches a halt condition.
    3. Emit those values and pause your stream.

    pausable (Demo)

    The pausable operator will emit values that match the halt condition and then stop the stream immediately.

    export function pausable(
      on$: Observable, // when on$ emits 'pausable' will emit values from the buffer and all incoming values 
      off$: Observable, // when off$ emits 'pausable' will stop emitting and buffer incoming values
      haltCondition: (value: T) => boolean, // if 'haltCondition' returns true for a value in the stream the stream will be paused
      pause: () => void, // pauses the stream by triggering the given on$ and off$ observables
      spread: boolean = true // if true values from the buffer will be emitted separately, if 'false' values from the buffer will be emitted in an array
    ) {
      return (source: Observable) => defer(() => { // defer is used so that each subscription gets its own buffer
        let buffer: T[] = [];
        return merge(
          source.pipe(
            bufferToggle(off$, () => on$),
            tap(values => buffer = buffer.concat(values)), // append values to your custom buffer
            map(_ => buffer.findIndex(haltCondition)), // find the index of the first element that matches the halt condition
            tap(haltIndex => haltIndex >= 0 ? pause() : null), // pause the stream when a value matching the halt condition was found
            map(haltIndex => buffer.splice(0, haltIndex === -1 ? customBuffer.length : haltIndex + 1)), // get all values from your custom buffer until a haltCondition is met
            mergeMap(toEmit => spread ? from(toEmit) : toEmit.length > 0 ? of(toEmit) : EMPTY) // optional value spread (what your mergeAll did)
          ),
          source.pipe(
            windowToggle(on$, () => off$),
            mergeMap(x => x),
            tap(value => haltCondition(value) ? pause() : null), // pause the stream when an unbuffered value matches the halt condition
          ),
        );
      });
    }
    

    You can adjust this operator to your specific needs e.g. use less input parameters and incorporate share into it, see this version with less parameters.

    Usage

    active$ = new BehaviorSubject(true);
    on$ = this.active$.pipe(filter(v => v));
    off$ = this.active$.pipe(filter(v => !v));
    
    interval(500).pipe(
      share(),
      pausable(on$, off$, v => this.active$.value && this.pauseOn(v), () => this.active$.next(false))
    ).subscribe(console.log);
    
    pauseOn = (value: number) => value > 0 && value % 10 === 0
    

    2. A fully custom buffer

    You can go with a fully custom approach using only one input observable similar to Brandon's approach.

    bufferIf (Demo)

    bufferIf will buffer incoming values when the given condition emits true and emits all values from the buffer or passes new ones through when the condition is false.

    export function bufferIf(condition: Observable) {
      return (source: Observable) => defer(() => {
        const buffer: T[] = [];
        let paused = false;
        let sourceTerminated = false;
        return merge( // add a custon streamId to values from the source and the condition so that they can be differentiated later on
          source.pipe(map(v => [v, 0]), finalize(() => sourceTerminated = true)),
          condition.pipe(map(v => [v, 1]))
        ).pipe( // add values from the source to the buffer or set the paused variable
          tap(([value, streamId]) => streamId === 0 ? buffer.push(value as T) : paused = value as boolean), 
          switchMap(_ => new Observable(s => {
            setTimeout(() => { // map to a stream of values taken from the buffer, setTimeout is used so that a subscriber to the condition outside of this function gets the values in the correct order (also see Brandons answer & comments)
              while (buffer.length > 0 && !paused) s.next(buffer.shift())
            }, 0)
          })), // complete the stream when the source terminated and the buffer is empty
          takeWhile(_ => !sourceTerminated || buffer.length > 0, true) 
        );
      })
    } 
    

    Usage

    pause$ = new BehaviorSubject(false);
    
    interval(500).pipe(
      bufferIf(this.pause$),
      tap(value => this.pauseOn(value) ? this.pause$.next(true) : null)
    ).subscribe(console.log);
    
    pauseOn = (value: number) => value > 0 && value % 10 === 0
    

提交回复
热议问题