简体   繁体   中英

Rxjs: Observable with takeUntil(timer) keeps emitting after the timer has ticked

I have run into a very strange behavior of takeUntil() . I create an observable timer:

let finish = Observable.timer(3000);

Then I wait for some time and call

// 2500 ms later
someObservable.takeUntil(finish);

I would expect said observable to stop emitting after the timer "ticks", ie about 500ms after its creation. In reality , way beyond the moment when the timer "ticks".,远远超过计时器“滴答”的那一刻。 This does not happen if I create the timer with a Date object containing absolute time value.

Is this by design? If yes, what is the explanation?

Here's complete code, runnable with node.js (it requires npm install rx ):

let {Observable, Subject} = require("rx")
let start = new Date().getTime();
function timeMs() { return new Date().getTime() - start };

function log(name, value) { 
    console.log(timeMs(), name, value);
}

Observable.prototype.log = function(name) {
    this.subscribe( v=>log(name,v), 
                    err=>log(name, "ERROR "+err.message), 
                    ()=>log(name, "DONE"));
    return this;
}

let finish = Observable.timer(3000).log("FINISH");
setTimeout( ()=>Observable.timer(0,500).takeUntil(finish).log("seq"), 2500);

This generates the following output:

2539 'seq' 0
3001 'FINISH' 0
3005 'FINISH' 'DONE'
3007 'seq' 1
3506 'seq' 2
4006 'seq' 3
4505 'seq' 4
5005 'seq' 5
5506 'seq' 6
5507 'seq' 'DONE'

If I create the timer using absolute time:

let finish = Observable.timer(new Date(Date.now()+3000)).log("FINISH");

Then it behaves as expected:

2533 'seq' 0
3000 'seq' 'DONE'
3005 'FINISH' 0
3005 'FINISH' 'DONE'

This behavior seems to be rather consistent in various situations.Eg if you take an interval and create child sequences using mergeMap() or switchMap() , the result would be similar: child sequences keep emitting beyond the finish event.

Thoughts?

You are forgetting the first rule of cold Observables : Each subscription is a new stream.

Your log operator has a bug; it is subscribing once to the Observable (thus creating the first subscription) and then returning the original Observable , which get subscribed to again , implicitly, when you pass it to the takeUntil operator. Thus in reality you actually have two seq streams active, both of which are behaving correctly.

It works in the absolute case, because you are basically setting each stream to emit at a specific time, not a relative time to when the subscription occurs.

If you want to see it work I would suggest you change your implementation to:

let start = new Date().getTime();
function timeMs() { return new Date().getTime() - start };

function log(name, value) { 
    console.log(timeMs(), name, value);
}

Observable.prototype.log = function(name) {
    // Use do instead of subscribe since this continues the chain
    // without directly subscribing.
    return this.do(
      v=>log(name,v), 
      err=>log(name, "ERROR "+err.message), 
      ()=>log(name, "DONE")
    );
}

let finish = Observable.timer(3000).log("FINISH");

setTimeout(()=> 
  Observable.timer(0,500)
    .takeUntil(finish)
    .log("seq")
    .subscribe(), 
2500);

For completeness, here's the code that actually does what I wanted. By using Observable.publish().connect() it creates a "hot" timer that starts ticking immediately, and keeps the same time for all subscribers. It also avoid unwanted subscriptions in the "log" method, as suggested by @paulpdaniels.

Warning: beware of the race condition. If the child sequence starts after the timer has ticked, it will never stop. To demonstrate, change timeout in the last line from 2500 to 3500.

let {Observable, Subject, Scheduler, Observer} = require("rx")
let start = new Date().getTime();
function timeMs() { return new Date().getTime() - start };

function log(name, value) { 
    console.log(timeMs(), name, value);
}

var logObserver =  function(name) {
    return Observer.create( 
      v=>log(name,v), 
      err=>log(name, "ERROR "+err.message), 
      ()=>log(name, "DONE"));
}

Observable.prototype.log = function(name) { return this.do(logObserver(name)); }

Observable.prototype.start = function() { 
    var hot = this.publish(); hot.connect(); 
    return hot; 
}

let finish = Observable.timer(3000).log("FINISH").start();

setTimeout(()=> 
  Observable.timer(0,500)
    .takeUntil(finish)
    .log("seq")
    .subscribe(), 
2500);

The output is

2549 'seq' 0
3002 'FINISH' 0
3006 'seq' 'DONE'
3011 'FINISH' 'DONE'

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM