I'm asking this because I tried below code:
//RxJs v5.5.2; NodeJs v7.10.1
var Rx = require('rxjs/Rx');
var observable = Rx.Observable.of(1,2,3);
var sub = observable.subscribe(console.log);
var sub1 = observable.subscribe(console.log);
It outputs 1 2 3 1 2 3 (new line omitted).
However, as RxJs's document says, Observable is a stream, so why does the second subscription got all the values? My understanding is, after the first subscribe, the observable has already been completed and as a stream, it should never produce a value it has already emitted.
Do I misunderstood anything?
From the article referenced above, Hot vs Cold Observables
Observables are just functions!
Observables are functions that tie an observer to a producer. That’s it. They don’t necessarily set up the producer, they just set up an observer to listen to the producer, and generally return a teardown mechanism to remove that listener. The act of subscription is the act of “calling” the observable like a function, and passing it an observer.
So, the observable is just a pipe between a producer and observer, and the producer can (often) have the state 'baked in'.
Trying to prove that there's a producer somewhere in your example Rx.Observable.of(1,2,3) is a bit difficult.
The source for Observable.of is
export const of = ArrayObservable.of;
and ArrayObservable.of
static of<T>(...array: Array<T | IScheduler>): Observable<T> {
so the spread operator is the producer in this case, and array is the state which is passed in to the Observable.
I can't find source for the spread operator, but this ref es6-equivalents-in-es5#spread-operator gives the the following es5 equivalent
var _toArray = function (arr) {
return Array.isArray(arr) ? arr : [].slice.call(arr);
};
function add(a, b) {
return a + b;
}
var nums = [5, 4];
console.log(add.apply(null, _toArray(nums)));
Assuming _toArray() above is a fair approximation of spread, we can argue that spread is essentially a producer function.
So, does Rx.Observable.of(1,2,3) have state? We could argue yes, since the statement encloses the state, but in the equivalent
const nums = [1,2,3]
const src = Rx.Observable.of(...nums)
we can see that the state is actually external to the observable.
What about hot?
A hot observable has a producer that's not reproducing it's values on demand, e.g events.
So when the producer is hot, the observable does not 'remember' state prior to the setting up of the pipeline (i.e when subscription occurs), so again the producer has the state not the observable.
console.clear()
const eventEmitter = new EventEmitter();
var source = Rx.Observable.fromEvent(eventEmitter, 'data')
eventEmitter.emit('data', 1);
eventEmitter.emit('data', 2);
source.subscribe(x => console.log('subscription', x))
eventEmitter.emit('data', 3);
eventEmitter.emit('data', 4);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.2/Rx.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/EventEmitter/5.2.4/EventEmitter.js"></script>
Related
I am trying to understand why share RxJs operator works differently if the source Observable is created with range instead of timer.
Changing the original code to:
const source = range(1, 1)
.pipe(
share()
)
const example = source.pipe(
tap(() => console.log('***SIDE EFFECT***')),
mapTo('***RESULT***'),
)
const sharedExample = example
const subscribeThree = sharedExample.subscribe(val => console.log(val))
const subscribeFour = sharedExample.subscribe(val => console.log(val))
Results in:
console.log src/pipeline/foo.spec.ts:223
SIDE EFFECT
console.log src/pipeline/foo.spec.ts:228
RESULT
console.log src/pipeline/foo.spec.ts:223
SIDE EFFECT
console.log src/pipeline/foo.spec.ts:229
RESULT
Basically, the side effect is invoked more than once.
As far as I know range is supposed to be a cold observable but it is said that share should turn cold observables to hot.
What is the explanation behind this behaviour ?
Two things to point out.
First, if you look closely at the function signature for range, you'll see it takes a third parameter, a SchedulerLike.
If unspecified, RxJS calls the next handler of each subscriber immediately with the relevant value for the range observable until it's exhausted. This isn't desirable if you intend to use the share operator, because it effectively bypasses any shared side effect processing that might be introduced.
Relevant snippet taken from the actual implementation:
// src/internal/observable/range.ts#L53
do {
if (index++ >= count) {
subscriber.complete();
break;
}
subscriber.next(current++);
if (subscriber.closed) {
break;
}
} while (true);
timer also takes an optional SchedulerLike argument. If unspecified, the implementation adopts AsyncScheduler by default, different to the default for range.
Secondly, the share operator should follow all other operators that might have side effects. If it precedes them, the expected unifying behaviour of pipe operator processing is lost.
So, with both points in mind, to make the share operator work with range as you're expecting:
const { asyncScheduler, range, timer } = rxjs;
const { mapTo, tap, share } = rxjs.operators;
// Pass in an `AsyncScheduler` to prevent immediate `next` handler calls
const source = range(1, 1, asyncScheduler).pipe(
tap(() => console.log('***SIDE EFFECT***')),
mapTo('***RESULT***'),
// All preceding operators will be in shared processing
share(),
);
const sub3 = source.subscribe(console.log);
const sub4 = source.subscribe(console.log);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.4.0/rxjs.umd.min.js"></script>
When I subscribe to a shared mapping from BehaviorSubject instance (t), only first subscription is executed.
When the original BehaviorSubject (obj) emits second value, only the latest value is printed, and both subscriptions were executed.
Let check my code
const obj = new Rx.BehaviorSubject(1)
obj.subscribe(console.log)
const t = obj.map(u => {
console.log("mapped")
return u * 10
}).share()
t.subscribe(x => console.log("subscribe 1 " + x))
t.subscribe(x => console.log("subscribe 2 " + x))
//with the following line un-commented, both subscriptions print out new value
//obj.next(2)
My expected result is
1
mapped
subscribe 1 10
subscribe 2 10
but the actual result was
1
mapped
subscribe 1 10
Sorry for the naive question. Is there anyone can explain this to me?
Thank you so much
Any operator (including the share) actually creates a new Sub-Observable, which has it's own share/replay-properties that are detached from the source-observable.
So to have your result, you should use publishReplay(1) instead of share().
(With publishReplay you of course have to either use refCount() or connect())
const obj = new Rx.BehaviorSubject(1)
obj.subscribe(console.log)
const t = obj.map(u => {
console.log("mapped")
return u * 10
}).publishReplay(1)
.refCount();
t.subscribe(x => console.log("subscribe 1 " + x))
t.subscribe(x => console.log("subscribe 2 " + x))
//with the following line un-commented, both subscriptions print out new value
//obj.next(2)
<script src="https://unpkg.com/rxjs/bundles/Rx.min.js"></script>
In your example you have two Subjects:
the BehaviorSubject in obj.
Subject instance inside .share().
Remember that BehaviorSubject emits its cached value only when you subscribe to it.
The first observer obj.subscribe(console.log) subscribes directly to the BehaviorSubject. This prints 1.
Then you create a chain t that ends with the share() operator.
Now you subscribe to t with t.subscribe. This means you subscribe to the Subject inside share() and since this is its first observer it needs to subscribe to its source Observable (which in turn reaches the source BehaviorSubject that emits its cached value). Note that share() is just a shortcut for using the multicast() operator with refCount().
And the last line you subscribe again with t.subscribe. Just like previously this subscribes to the Subject inside share(). However share() already is subscribed to its source Observable so it doesn't make another subscription. That's the point of multicasting and the multicast() operator.
That's why you won't see any subscribe 2 10 and you won't event see mapped printed twice. You're subscribing to the Subject inside share(), not the source BehaviorSubject.
Is there an operator in RxJS that debounces without delaying the "first event in a burst", but delaying (and always emitting) the "last event in a burst"?
Something like this:
---a----b-c-d-----e-f---
after awesome-debounce(2 dashes) becomes:
---a----b------d--e----f
while a normal debounce would be:
-----a---------d-------f
It's kind of a mix between throttle and debounce...
Hmmm, this is the easiest solution I can think of. The interesting part for you is the awesomeDebounce() function that creates the sub-chain.
It basically just combines throttle() and debounceTime() operators:
const Rx = require('rxjs');
const chai = require('chai');
let scheduler = new Rx.TestScheduler((actual, expected) => {
chai.assert.deepEqual(actual, expected);
console.log(actual);
});
function awesomeDebounce(source, timeWindow = 1000, scheduler = Rx.Scheduler.async) {
let shared = source.share();
let notification = shared
.switchMap(val => Rx.Observable.of(val).delay(timeWindow, scheduler))
.publish();
notification.connect();
return shared
.throttle(() => notification)
.merge(shared.debounceTime(timeWindow, scheduler))
.distinctUntilChanged();
}
let sourceMarbles = '---a----b-c-d-----e-f---';
let expectedMarbles = '---a----b------d--e----f';
// Create the test Observable
let observable = scheduler
.createHotObservable(sourceMarbles)
.let(source => awesomeDebounce(source, 30, scheduler));
scheduler.expectObservable(observable).toBe(expectedMarbles);
scheduler.flush();
The inner notification Observable is used only for the throttle() operator so I can reset its timer manually when I need. I also had to turn this Observable into "hot" to be independent on the internal subscriptions from throttle().
That's indeed useful debounce type for many situations. Use merge, throttleTime and debounceTime in a next way:
Rx.Observable.merge(source.throttleTime(1000), source.debounceTime(2000))
Full example is here http://jsbin.com/godocuqiwo/edit?js,console
Note: it will emit not only first and last value in debounce interval but also values produced by throttle (which is usually expected and needed, as for scroll debouncing for example).
I have a question regarding multicasted observables and an unexpected (for me) behaviour I noticed.
const a = Observable.fromEvent(someDom, 'click')
.map(e => 1)
.startWith(-1)
.share();
const b = a.pairwise();
a.subscribe(a => {
console.log(`Sub 1: ${a}`);
});
a.subscribe(a => {
console.log(`Sub 2: ${a}`)
});
b.subscribe(([prevA, curA]) => {
console.log(`Pairwise Sub: (${prevA}, ${curA})`);
});
So, there is a shared observable a, which emits 1 on every click event. -1 is emitted due to the startWith operator.
The observable b just creates a new observable by pairing up latest two values from a.
My expectation was:
[-1, 1] // first click
[ 1, 1] // all other clicks
What I observed was:
[1, 1] // from second click on, and all other clicks
What I noticed is that the value -1 is emitted immediately and consumed by Sub 1, before even Sub 2 is subscribed to the observable and since a is multicasted, Sub 2 is too late for the party.
Now, I know that I could multicast via BehaviourSubject and not use the startWith operator, but I want to understand the use case of this scenario when I use startWith and multicast via share.
As far as I understand, whenever I use .share() and .startWith(x), only one subscriber will be notified about the startWith value, since all other subscribers are subscribed after emitting the value.
So is this a reason to multicast via some special subject (Behavior/Replay...) or am I missing something about this startWith/share scenario?
Thanks!
This is actually correct behavior.
The .startWith() emits its value to every new subscriber, not only the first one. The reason why b.subscribe(([prevA, curA]) never receives it is because you're using multicasting with .share() (aka .publish().refCount()).
This means that the first a.subscribe(...) makes the .refCount() to subscribe to its source and it'll stay subscribed (note that Observable .fromEvent(someDom, 'click') never completes).
Then when you finally call b.subscribe(...) it'll subscribe only to the Subject inside .share() and will never go through .startWith(-1) because it's multicasted and already subscribed in .share().
I need to create a new instance operator on streams with the following characteristics
Signature
Rx.Observable.prototype.scan_with_reset(accumulator, seed$)
where :
Arguments
accumulator (Function): An accumulator function to be invoked on each element.
seed$ (Observable) : An observable whose values will be used to restart the accumulator function. The accumulator function has the following signature function accumulator_fn(accumulator_state, source_value). I want the value in seed$ to reset accumulator_state to the seed value and emit the seed value.
Returns
(Observable) : An observable sequence which results from the comonadic bind operation (whatever that means, I am copying Rxjs documentation here). Vs. the normal scan operator, what happens here is that when the accumulator function is 'restarted' from the seed value emitted by the seed$ observable, that seed value is emitted, and the next value to be emitted by the scan_with_reset operator will be accumulator_fn(seed, source_value)
Example of use :
var seed$ = Rx.Observable.fromEvent(document, 'keydown')
.map(function(ev){return ev.keyCode})
.startWith(0);
var result$ = counter$.scan_with_reset(seed$,
function accumulator_fn (acc, counter) {return acc+counter});
The following diagrams should explain more in details the expected results:
seed : 0---------13--------27------------
counter : -1--5--2----6---2-----4---1---3---
result : 0-1--6--8-13-19--21-27-31--32--35-
My initial attempt to do this was to modify the accumulator_fn to have the seed$ modify a variable that would in the scope of accumulator_fn so I can detect changes in the function itself.
I pursue two goals here:
have an implementation which is as stateless and closure-less as possible
understand the mechanics behind defining one's own operators on
streams, of which this would be hopefully a simple example
I had a look at scan source code : https://github.com/Reactive-Extensions/RxJS/blob/master/src/core/linq/observable/scan.js
but I am not sure where to go from there.
Does anybody has any experience in creating Rxjs stream operators? What are the conventions to follow and traps to avoid? Are there any examples of custom-made operators that I could look at? How would you go about implementing this particular one?
[UPDATE] : Some test code for the accepted answer
var seed$ = Rx.Observable.fromEvent(document, 'keydown')
.map(function(ev){return ev.keyCode})
.startWith(0);
var counter$ = Rx.Observable.fromEvent(document, 'mousemove')
.map(function(ev){return 1});
var result$ = counter$.scanWithReset(seed$,
function accumulator_fn (acc, counter) {return acc+counter});
var s = function (x) {console.log("value: ", x)};
var disposable = result$.subscribe(s)
Moving the mouse should show a value increase by 1, and pressing a key should restart the counter with the value of the key pressed.
As a general case when creating operators it is generally easiest to use the Observable.create method which essentially defines how your Observable should behave when it is subscribed to or just wrap an existing set of operators ala share.
When you get more into performance there are some other considerations (Observable.create is not terribly efficient at scale) and you could look into creating a custom Observable like map.
For your case I would recommend the former for right now. I would think of your problem really as several independent streams that we would like to flatten into a single stream. Each new stream will start when reset is triggered. This is really sounding an awful lot like flatMap to me:
Rx.Observable.prototype.scanWithReset = function ($reset, accum, seed) {
var source = this;
//Creates a new Observable
return Rx.Observable.create(function (observer) {
//We will be reusing this source so we want to make sure it is shared
var p = source.publish();
var r = $reset
//Make sure the seed is added first
.startWith(seed)
//This will switch to a new sequence with the associated value
//every time $reset fires
.flatMapLatest(function (resetValue) {
//Perform the scan with the latest value
return source.scan(accum, resetValue);
});
//Make sure every thing gets cleaned up
return new Rx.CompositeDisposable(
r.subscribe(observer),
//We are ready to start receiving from our source
p.connect());
});
}