8
votes

I created a class which sets up a pausable RxJS Observable using the interval operator:

export class RepeatingServiceCall<T> {
  private paused = false;
  private observable: Observable<T>;
  constructor(serviceCall: () => Observable<T>, delay: number) {
    this.observable = interval(delay).pipe(flatMap(() => (!this.paused ? serviceCall() : NEVER)));
  }
  setPaused(paused: boolean) {
    this.paused = paused;
  }
  getObservable() {
    return observable;
  }
}

This seems to work fine, but the problem I am trying to solve is that I want the timer to reset when unpaused. So, let's say that the interval time is 10 seconds and 5 seconds after the last time the interval emitted, setPaused(false) is called. In that scenario, I want it to emit immediately and then restart the timer.

Would something like that be an easy thing to add?

5
do you want that immediate emission after unpause to happen only if certain time has passed or in any case when unpaused? - kos

5 Answers

21
votes

If you use timer instead of interval, and set the initial delay to 0, then your interval will fire immediately.

You can use takeUntil operator to prevent the interval to run always, and repeatWhen operator to restart it whenever you want:

import { Observable, Subject, timer } from 'rxjs';
import { repeatWhen, switchMap, takeUntil } from 'rxjs/operators';

export class RepeatingServiceCall<T> {
  readonly observable$: Observable<T>;
  private readonly _stop = new Subject<void>();
  private readonly _start = new Subject<void>();

  constructor(serviceCall: () => Observable<T>, delay: number) {
    this.observable$ = timer(0, delay)
      .pipe(
        switchMap(() => serviceCall()),
        takeUntil(this._stop),
        repeatWhen(() => this._start)
      );
  }
  start(): void {
    this._start.next();
  }
  stop(): void {
    this._stop.next();
  }
}

Here is a working StackBlitz example.

P.S.: Getters and setters are working different in typescript. So you do not need classic getter concept, you can just make the attribute public and readonly.

2
votes

You can achieve the behavior you are describing with the following snippet:

const delay = 1000;

const playing = new BehaviorSubject(false);

const observable = playing.pipe(
  switchMap(e => !!e ? interval(delay).pipe(startWith('start')) : never())
);

observable.subscribe(e => console.log(e));

// play:
playing.next(true);

// pause:
playing.next(false);
  • When the playing Observable emits true, the switchMap operator will return a new interval Observable.
  • Use the startWith operator to emit an event immediately when unpausing.
  • If you wish to have the interval start automatically when subscribing to the observable, then simply initialize the BehaviorSubject with true.

StackBlitz Example

1
votes

Yet another approach with a switchMap:

const { fromEvent, timer } = rxjs;
const { takeUntil, switchMap, startWith } = rxjs.operators;

const start$ = fromEvent(document.getElementById('start'), 'click');
const stop$ = fromEvent(document.getElementById('stop'), 'click');


start$.pipe(
  startWith(void 0), // trigger emission at launch
  switchMap(() => timer(0, 1000).pipe(
    takeUntil(stop$)
  ))
).subscribe(console.log);
<script src="https://unpkg.com/[email protected]/bundles/rxjs.umd.min.js"></script>

<button id="start">start</button>
<button id="stop">stop</button>

And a simpler one, that merges start and stop Observables to switch off them:

const { fromEvent, merge, timer, NEVER } = rxjs;
const { distinctUntilChanged, switchMap, mapTo, startWith } = rxjs.operators;

const start$ = fromEvent(document.getElementById('start'), 'click');
const stop$ = fromEvent(document.getElementById('stop'), 'click');


merge(
  start$.pipe(mapTo(true), startWith(true)),
  stop$.pipe(mapTo(false))
).pipe(
  distinctUntilChanged(),
  switchMap(paused => paused ? timer(0, 1000) : NEVER)
)
.subscribe(console.log);
<script src="https://unpkg.com/[email protected]/bundles/rxjs.umd.min.js"></script>

<button id="start">start</button>
<button id="stop">stop</button>

And another, even wierder approach, using repeat() :

const { fromEvent, timer } = rxjs;
const { take, concatMap, takeUntil, repeat } = rxjs.operators;

const start$ = fromEvent(document.getElementById('start'), 'click');
const stop$ = fromEvent(document.getElementById('stop'), 'click');


start$.pipe(
  take(1),
  concatMap(()=>timer(0, 1000)),
  takeUntil(stop$),
  repeat()
).subscribe(console.log);
<script src="https://unpkg.com/[email protected]/bundles/rxjs.umd.min.js"></script>

<button id="start">start</button>
<button id="stop">stop</button>

Just wanted to join this party :)

0
votes

You can abandon the old timer on start and start a new one on start.

const { interval, Subject, fromEvent } = rxjs;
const { takeUntil } = rxjs.operators;

let timer$;

const pause = new Subject();

const obs$ = new Subject();

obs$.subscribe(_ => { console.log('Timer fired') });

function start() {
  timer$ = interval(1000);
  timer$.pipe(takeUntil(pause)).subscribe(_ => { obs$.next(); });
}

function stop() {
  pause.next();
  timer$ = undefined;
}

fromEvent(document.getElementById('toggle'), 'click').subscribe(() => {
  if (timer$) {
    stop();
  } else {
    start();
  }
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.4.0/rxjs.umd.min.js"></script>
<button id="toggle">Start/Stop</button>
0
votes

check this code

/**
* it is a simple timer created by via rxjs
* @author KentWood
* email [email protected]
*/
function rxjs_timer(interval, times, tickerCallback, doneCallback, startDelay) {
    this.pause = function () {
        this.paused = true;
    }
    this.resume = function () {
        this.paused = false;
    }
    this.stop = function () {
        if (this.obs) {
            this.obs.complete();
            this.obs.unsubscribe();
        }
        this.obs = null;

    }
    this.start = function (interval, times, tickerCallback, doneCallback, startDelay) {
        this.startDelay = startDelay || 0;
        this.interval = interval || 1000;
        this.times = times || Number.MAX_VALUE;
        this.currentTime = 0;
        this.stop();

        rxjs.Observable.create((obs) => {
            this.obs = obs;
            let p = rxjs.timer(this.startDelay, this.interval).pipe(
                rxjs.operators.filter(() => (!this.paused)), 
              rxjs.operators.tap(() => {
                    if (this.currentTime++ >= this.times) {
                        this.stop();
                    }
                }),
              rxjs.operators.map(()=>(this.currentTime-1))
            );
            let sub = p.subscribe(val => obs.next(val), err => obs.error(err), () => obs
                .complete());
            return sub;
        }).subscribe(tickerCallback, null, doneCallback);
    }
    this.start(interval, times, tickerCallback, doneCallback, startDelay);
}
/////////////test/////////////
var mytimer = new rxjs_timer(
1000/*interval*/, 
10 /*times*/, 
(v) => {logout(`time:${v}`)}/*tick callback*/, 
() => {logout('done')}/*complete callback*/, 
2000/*start delay*/);

//call mytimer.pause() 
//call mytimer.resume() 
//call mytimer.stop() 




function logout(str){
  document.getElementById('log').insertAdjacentHTML( 'afterbegin',`<p>${str}</p>`)
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.1/rxjs.umd.js"></script>

<button onclick="mytimer.pause()"> pause</button>
<button onclick="mytimer.resume()"> resume</button>
<button onclick="mytimer.stop()"> stop</button>
<div id='log'></div>