6

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?

Good Night Nerd Pride
  • 8,245
  • 4
  • 49
  • 65

3 Answers3

15

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.

Brandon
  • 38,310
  • 8
  • 82
  • 87
  • Oh snap, of course! Now I feel stupid. I could have noticed it in the example with `fromEvent`, because double clicks also appear as double clicks in the visualization. Thanks for the detailed run-down. – Good Night Nerd Pride Jul 09 '19 at 19:09
  • @Brandon How will you incrementally increase RxJS delay without using interval? https://stackoverflow.com/q/64625561/3073280 – Jek Oct 31 '20 at 19:20
1

In first code snippet you are emitting an array element by element. First delay, then array elements are handled.

'from' and 'pipe' make 'delay' perform once. Pipe sequences processing, first delay, then tap, tap, tap, tap.

In second code snippet you are emitting objects (they arrive), so delay happens once for each object.

'fromEvent' and 'pipe' make 'delay' per event. Pipe sequences processing of delay before each event.

Riad Baghbanli
  • 3,105
  • 1
  • 12
  • 20
  • That's not how `from` works. You're talking about `of`. See the updated first snippet. – Good Night Nerd Pride Jul 08 '19 at 17:01
  • Sorry, I still don't understand. You say "'from' and 'pipe' make 'delay' perform once", but why is that the case? Also, if you check the console, you see that the `console.log` is called _before_ `delay` is applied. In my understanding the operators in `pipe` are applied every time `next` is called on the observer/subscriber. And since `from` [calls `next` for every item](https://github.com/ReactiveX/rxjs/blob/master/src/internal/util/subscribeToArray.ts) I expect the delay to happen for each item. – Good Night Nerd Pride Jul 08 '19 at 20:35
  • Nothing in OP's stream ever emits an array, each value in the array is emitted individually. That is what from does with an array, of would emit an array. – Adrian Brand Jul 09 '19 at 09:46
1

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>
Adrian Brand
  • 20,384
  • 4
  • 39
  • 60
  • But the problem is that the values generated with `from` are all emitted _at once_ after the initial delay. Instead I want a delay before each of them, which is how `fromEvent` does it. – Good Night Nerd Pride Jul 09 '19 at 07:24
  • That is how from works, it creates a stream that emits the values straight away. The delay just delays one second from when it receives the emit from the observable before it. If you want values that emit one second after each other then use an interval. – Adrian Brand Jul 09 '19 at 09:44
  • What is a stream "that emits values straight away"? As I understand it an `Observable` can only emit by calling `next` and each `next`ed value is applied to the operators in `pipe`. You make it sound like there is a special kind of `next` that prevents the application of the `pipe` operators except for the value emitted first. – Good Night Nerd Pride Jul 09 '19 at 16:59
  • 1
    Observables are streams, when you create one from from([1,2,3,4]) it calls next(1), next(2), next(3) and next(4) all in a row with no delay between them. Adding the delay operator delay the emit by a second but they have all been emitted at the same time. – Adrian Brand Jul 09 '19 at 22:03
  • Just hit run code snippet on those two above and see the difference that putting the tap before the delay and after the delay does. – Adrian Brand Jul 09 '19 at 22:04