Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Confusing behavior of rxjs operator `delay`

I'm a bit confused about the rxjs operator delay.

When I test it with a fake observable created with from, then I only see an initial delay:

const { from } = Rx;
const { delay, tap } = RxOperators;

from([1, 2, 3, 4]).pipe(
  tap(console.log),
  delay(1000));

(You can copy & paste this code snippet into rxviz.)

I placed a tap in there to make sure from actually emits the array items as separate values instead of a single array value.

An initial delay is not what I expected, but at least that's what the docs say:

[...] this operator time shifts the source Observable by that amount of time expressed in milliseconds. The relative time intervals between the values are preserved.

However, when I test it with an observable created from an event, then I see a delay before each emitted value:

const { fromEvent } = Rx;
const { delay } = RxOperators;

fromEvent(document, 'click')
  .pipe(delay(1000))

What's going on here? Why is delay behaving differently in both cases?

like image 350
Good Night Nerd Pride Avatar asked Dec 17 '22 16:12

Good Night Nerd Pride


2 Answers

All delay does is what it says: whenever it receives a value, it holds on to that value for the delay period, then emits it. It does the same thing for each value it receives. delay does not change the relative timings between items in the stream.

So, when you do from([1,2,3,4]).pipe(delay(1000)), what happens is:

  • Time 0: from emits 1
  • Time 0: delay sees 1 and starts timer1
  • Time 0: from emits 2
  • Time 0: delay sees 2 and starts timer2
  • ...
  • Time 1000: timer1 completes and delay emits 1
  • Time 1000: timer2 completes and delay emits 2
  • ...

So because all 4 values were emitted in rapid succession, you really only see an initial delay and then all 4 values get emitted downstream. In reality, each value was delayed by 1 second from when it was originally emitted.

If you want to "spread apart" the items so that they are at least 1 second apart, then you could do something like:

const source = from([1, 2, 3, 4])
const spread = source.pipe(concatMap(value => of(value).pipe(delay(1000))));
spread.subscribe(value => console.log(value));

This converts each individual value into an observable that emits the value after a delay, then concatenates these observables. This means the timer for each item will not start ticking until the previous item's timer finishes.

like image 124
Brandon Avatar answered Feb 26 '23 17:02

Brandon


You tap the stream and get the values that are emitted then you pipe them into delay which emits them one second later. Each function in the pipe returns a new observable which emits a value to the next function in the pipe. Tap returns the same observable that has not been delayed yet and delay returns an observable that emits one second later.

const { from } = rxjs;
const { delay, tap } = rxjs.operators;

from([1, 2, 3, 4]).pipe(
  tap(val => { console.log(`Tap: ${val}`); }),
  delay(1000)).subscribe(val => { console.log(`Sub: ${val}`); });
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.2/rxjs.umd.min.js"></script>

If you put the tap after the delay then you see them after the delay.

const { from } = rxjs;
const { delay, tap } = rxjs.operators;

from([1, 2, 3, 4]).pipe(
  delay(1000),
  tap(val => { console.log(`Tap: ${val}`); })).subscribe(val => { console.log(`Sub: ${val}`); });
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.2/rxjs.umd.min.js"></script>
like image 20
Adrian Brand Avatar answered Feb 26 '23 17:02

Adrian Brand