4

I have a stream of data being long polled every 10 seconds. Long pol can be paused and be continued.

Problem is: It starts over when It continues. I have to make It continue from the second It was paused.

So if:

delay(10000)

paused at (7000)

Then when I start the long polling again my data has to be pulled after (10000-7000)=3000

Today is the fourth day I have been stuck with this problem and going insane..

I have created this blitz

https://stackblitz.com/edit/ang7-working-rxjs-stream-timer-combined?file=src/app/app.component.ts

Created a flow and a timer which pulls data. When flow and timer stops, sets the time that was left along with the new delay. But still when I press start It doesnt wait 3 more seconds It just cals the service right away which I dont want

Oguzhan
  • 724
  • 6
  • 12

3 Answers3

1

Here is an extremely rudimentary implementation relying on timer function and take, filter and takeUntil operators.

The solution in my view isn't too elegant. It depends on state variables paused and first and has a recursive trigger inside the next callback in the subscription. Also note it doesn't at the moment do any error handling. The polling completely stops in that case.

Controller

export const REFRESH_INTERVAL = 11;

@Component({ ... })
export class AppComponent implements OnDestroy {
  pause$ = new Subject();
  reset$ = new Subject();
  closed$ = new Subject();
  counter = REFRESH_INTERVAL;
  res: any;
  paused = false;

  constructor(private freeApiService: freeApiService) {}

  ngOnInit() {
    this.init();
  }

  init() {
    let first = true;
    this.reset$
      .pipe(
        switchMap(_ =>
          timer(0, 1000).pipe(
            take(this.counter),
            tap(_ => (this.paused = this.paused ? false : this.paused)),
            takeUntil(this.pause$),
            filter(_ => first || --this.counter === 0),
            finalize(
              () => (this.paused = this.counter != 0 ? true : this.paused)
            ),
            switchMap(_ => this.freeApiService.getDummy())
          )
        ),
        takeUntil(this.closed$)
      )
      .subscribe({
        next: res => {
          first = false;
          this.res = res;
          this.counter = REFRESH_INTERVAL;
          this.reset$.next();
        },
        error: error => console.log("Error fethcing data:", error)
      });
  }

  ngOnDestroy() {
    this.closed$.next();
  }
}

Template

<div my-app>
    <ng-container *ngIf="res; else noRes">
        <button (mouseup)="paused ? reset$.next() : pause$.next()">
      {{ paused ? 'Resume poll' : 'Pause poll' }}
    </button>

        <br><br>
        Refreshing in: {{ counter }}
        <br>
    Response:
        <pre>{{ res | json }}</pre>
    </ng-container>

    <ng-template #noRes>
        <button (mouseup)="reset$.next()">
      Start poll
    </button>
    </ng-template>
</div>

I've modified your Stackblitz

ruth
  • 29,535
  • 4
  • 30
  • 57
  • This is really great! Thank you so much! A quick question; Would this approach cause memory leakage besides making sure to subscribe only once? – Oguzhan Nov 06 '20 at 11:38
  • The `takeUntil(this.closed$)` should in theory closed the open subscription when the component is closed. So I'd say no, it wouldn't cause any memory leak. – ruth Nov 06 '20 at 12:07
  • If I use a slow responding api endpoint to this code. It repeats the request until one of them resolves and that is what I wanted to prevent in the first place. How can I fix that? – Oguzhan Nov 10 '20 at 13:57
1

I'd use the combination of interval, filter and scan operators with check frequency being set to 1s:

const pauserSubject = new BehaviorSubject(true);

const s$ = interval(1000).pipe(
  withLatestFrom(pauserSubject),
  filter(([intervalValue, isAllowed]) => isAllowed),
  scan(acc => acc + 1, 0),
  tap(emissionNumber => console.log('check attempt #', emissionNumber, ' on ', new Date().toTimeString())),
  filter(emissionCount => !(emissionCount % 10)),
  switchMapTo(of('request/response'))
);

Stackblitz example here

In this case the regular interval sets the "beat" checking the pause being true or false every 1s. After it's unpaused, the corresponding filter operator allows to proceed. The scan verifies that every 10th check (or every 10th second) will result a http call.

For your scenario:

  1. You start polling every 10 seconds
  2. You pause at the 3rd second for one second long
  3. The counter ensures that 7 more checks should be made (resulting 7 seconds of waiting)
  4. The http call is made on the 11th second (10 regular + 1 second of waiting)
Yevhenii Dovhaniuk
  • 1,073
  • 5
  • 11
1

This boils down to having a "pausable timer". Once you have that, you can just use the repeat operator to re-run it whenever it completes:

somePausableTimer$.pipe(
    repeat(),
    switchMap(() => of('polling time'))
)

If you care about time precision, here's my take on "pausable timer" - I think it's about as precise as you can get with javascript, but without the performance penalty that comes with solutions such as "check the pause state every 10 ms":

import {delay, distinctUntilChanged, filter, map, mapTo, pairwise, repeat, 
    scan, startWith, switchMap, take, withLatestFrom} from 'rxjs/operators';
import {defer, fromEvent, NEVER, Observable, of} from 'rxjs';

function pausableTimer(timerSpan: number, isActive$: Observable<boolean>): Observable<boolean> {

    const activeState$ = isActive$.pipe(
        distinctUntilChanged(),
        startWith(true, true),
        map(isActive => ({
            isActive,
            at: Date.now()
        }))
    );

    const pauseSpans$ = activeState$.pipe(
        pairwise(),
        filter(([,curr]) => curr.isActive),
        map(([prev, curr]) => curr.at - prev.at)
    );

    const accumulatedPauseSpan$ = pauseSpans$.pipe(
        scan((acc, curr) => acc += curr, 0)
    );

    return defer(() => {
        const startTime = Date.now();
        const originalEndTime = startTime + timerSpan;

        return activeState$.pipe(
            withLatestFrom(accumulatedPauseSpan$),
            switchMap(([activeState, accPause]) => {
                if (activeState.isActive) {
                    return of(true).pipe(
                        delay(originalEndTime - Date.now() + accPause)
                    );
                }
                else {
                    return NEVER;
                }
            }),
            take(1)
        );
    });
}

The function accepts a timerSpan in ms (in your case 10000) and an isActive$ observable which emits true/false for resume/pause. So to put it together:

const isActive$ = fromEvent(document, 'click').pipe(scan(acc => !acc, true)); // for example

pausableTimer(10000, isActive$).pipe(
    repeat(),
    switchMap(() => of('polling time'))
).subscribe();
Eden Ilan
  • 176
  • 1
  • 5