Search code examples
rxjsreactivexrxjs-pipeable-operators

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?


Solution

  • 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.