The task
Suppose we implement an Angular service and need to publish an Observable<number[]> to the world:
numbers: Observable<number[]>;
We want subscribers to:
receive the last value upon subscription
receive the whole modified array every time we change and publish it
Since #1, internally the Observable<number[]> should be "at least" a BehaviorSubject<number[]>.
But what about #2? Let's suppose we need to implement a method publishNumbersChange() which is called whenever we need to change and publish the changed array:
private publishNumbersChange() {
// Get current numbers array
...
changeArray();
// Now publish changed numbers array
...
}
The question
What is the RxJS 5 pattern to implement the task of publishing modified array based on its previous items?
Since I'm asking it mainly because currently I'm doing Angular stuff, here is the second part of the question:
What code does Angular (and the like frameworks based on RxJS) use when they provide an Observable which type parameter is an array to subsequently publish updated array?
Do they just keep a copy of the currently published array separately?
Some thoughts
It seems that storing the underlying array separately, so we always have access to it, is the simplest thing. But at the same time it does not look like a RxJS way (need to have a state outside a RxJS stream).
On the other hand, we could do something like the following:
private publishNumbersChange() {
// To get the latest value from the stream, we have to subscribe
const subscription: Subscription = this.numbers.subscribe((numbers: number[]) => {
// We got the last value in stream in numbers argument. Now make changes to the array
changeArray();
// And push it back as a new value to the stream
this.numbers.next(numbers);
});
// Also we have to unsubscribe
subscription.unsubscribe();
}
I see here at least one issue (not counting the complexity\reusability): "race condition" between executing subscription callback and unsubscribing. Looking at that code you can't tell for sure whether the callback would be actually executed. So it doesn't look as a proper way of doing this either.
It sounds like the operator you may be looking for is scan.
let arraySubject = new BehaviorSubject([]);
let array$ = arraySubject.scan((fullArray, newValue) => fullArray.concat([newValue]), [])
Scan accumulates values over time in an observable stream, and each item in the stream gets the last emitted value and the current value as parameters. executes a function on them and then emits the result. the above example takes a new value and appends it to your full array, the second parameter initializes it to an empty array.
This is clearly kind of restricting though since it only does ONE thing, which may not be robust enough. in this case you need to get clever:
let arraySubject = new BehaviorSubject([]);
let array$ = arraySubject.scan((fullArray, {modifier, payload}) => modifier(fullArray, payload), []);
Now you're passing in an "action" which has a modifier function, which defines how you want to modify the full array, and a payload of any additional data the modifier might need to go into the modifier function along with the full array
so you might do:
let modifier = (full, item) => full.splice(full.indexOf(item), 1);
arraySubject.next({modifier, payload: itemToRemove});
which removes the item you sent through. You can extend this pattern to literally any array modification.
A "gotcha" with scan though is that subscribers only get the accumulated value from the TIME THEY SUBSCRIBED. So, this will happen:
let arraySubject = new BehaviorSubject([]);
let array$ = arraySubject.scan((fullArray, {modifier, payload}) => modifier(fullArray, payload), []);
let subscriber1 = array$.subscribe();
//subscriber1 gets []
let modifier = (full, val) => full.concat([val]);
arraySubject.next({modifier, payload:1});
//subscriber1 gets [1]
arraySubject.next({modifier, payload:2});
//subscriber1 gets [1,2]
let subscriber2 = array$.subscribe();
//subscriber2 gets [2]
arraySubject.next({modifier, payload:3});
//subscriber1 gets [1,2,3]
//subscriber2 gets [2,3]
See what happened there? the only thing stored in the behaviorsubject was the second event, not the full array, scan is storing the full array, so the second subscriber only gets the second action since it wasn't subscribed during the 1st action. So you need a persistent subscriber pattern:
let arraySubject = BehaviorSubject([]);
let arrayModifierSubject = new Subject();
arrayModifierSubject.scan((fullArray, {modifier, payload}) => modifier(fullArray, payload), []).subscribe(arraySubject);
and you modify by calling next on arrayModifierSubject:
let modifier = (full, val) => full.concat([val]);
arrayModifierSubject.next({modifier, payload: 1});
and your subscribers get the array from the array source:
subscriber1 = arraySubject.subscribe();
In this set up, all array modifications go through the modifier subject who in turns broadcasts it to the behaviorsubject who stores the full array for future subscribers and broadcasts it to current subscribers. The behaviorsubject (the store subject) is persistently subscribed to the modifier subject (the action subject), and is the ONLY subscriber to the action subject, so the full array is never lost as the entire history of actions is always maintained.
some sample usages (with the above set up):
// insert 1 at end
let modifier = (full, value) => full.concat([value]);
arrayModifierSubject.next({modifier, payload: 1});
// insert 1 at start
let modifier = (full, value) => [value].concat(full);
arrayModifierSubject.next({modifier, payload: 1});
// remove 1
let modifier = (full, value) => full.splice(full.indexOf(value),1);
arrayModifierSubject.next({modifier, payload: 1});
// change all instances of 1 to 2
let modifier = (full, value) => full.map(v => (v === value.target) ? value.newValue : v);
arrayModifierSubject.next({modifier, payload: {target: 1, newValue: 2}});
you can wrap any of these functions in a "publishNumbersChange" function. How you exactly implement this depends on your needs, you can make functions like:
insertNumber(numberToInsert:number) => {
let modifier = (full, val) => full.concat([val]);
publishNumbersChange(modifier, numberToInsert);
}
publishNumbersChange(modifier, payload) => {
arrayModifierSubject.next({modifier, payload});
}
or you can declare an interface and make classes and use that:
publishNumbersChange({modifier, payload}) => {
arrayModifierSubject.next({modifier, payload});
}
interface NumberArrayModifier {
modifier: (full: number[], payload:any) => number[];
payload: any;
}
class InsertNumber implements NumberArrayModifier {
modifier = (full: number[], payload: number): number[] => full.concat([payload]);
payload: number;
constructor(numberToInsert:number) {
this.payload = numberToInsert;
}
}
publishNumbersChange(new InsertNumber(1));
And you can also extend similar functionality to any array modification. One last protip: lodash is a huge help with defining your modifiers in this type of system
so, how might this look in an angular service context?
This is a very simple implementation that isn't highly reusable, but other implementations could be:
const INIT_STATE = [];
#Injectable()
export class NumberArrayService {
private numberArraySource = new BehaviorSubject(INIT_STATE);
private numberArrayModifierSource = new Subject();
numberArray$ = this.numberArraySource.asObservable();
constructor() {
this.numberArrayModifierSource.scan((fullArray, {modifier, payload?}) => modifier(fullArray, payload), INIT_STATE).subscribe(this.numberArraySource);
}
private publishNumberChange(modifier, payload?) {
this.numberArrayModifierSource.next({modifier, payload});
}
insertNumber(numberToInsert) {
let modifier = (full, val) => full.concat([val]);
this.publishNumberChange(modifier, numberToInsert);
}
removeNumber(numberToRemove) {
let modifier = (full, val) => full.splice(full.indexOf(val),1);
this.publishNumberChange(modifier, numberToRemove);
}
sort() {
let modifier = (full, val) => full.sort();
this.publishNumberChange(modifier);
}
reset() {
let modifier = (full, val) => INIT_STATE;
this.publishNumberChange(modifier);
}
}
Usage here is simple, subscribers just subscribe to numberArray$ and modify the array by calling functions. You use this simple pattern to extend functionality however you like. This controls access to your number array and makes sure it is always modified in ways defined by the api and your state and your subject are always one in the same.
OK but how is this made generic/reusable?
export interface Modifier<T> {
modifier: (state: T, payload:any) => T;
payload?: any;
}
export class StoreSubject<T> {
private storeSource: BehaviorSubject<T>;
private modifierSource: Subject<Modifier<T>>;
store$: Observable<T>;
publish(modifier: Modifier<T>): void {
this.modifierSource.next(modifier);
}
constructor(init_state:T) {
this.storeSource = new BehaviorSubject<T>(init_state);
this.modifierSource = new Subject<Modifier<T>>();
this.modifierSource.scan((acc:T, modifier:Modifier<T>) => modifier.modifier(acc, modifier.payload), init_state).subscribe(this.storeSource);
this.store$ = this.storeSource.asObservable();
}
}
and your service becomes:
const INIT_STATE = [];
#Injectable()
export class NumberArrayService {
private numberArraySource = new StoreSubject<number[]>(INIT_STATE);
numberArray$ = this.numberArraySource.store$;
constructor() {
}
insertNumber(numberToInsert: number) {
let modifier = (full, val) => full.concat([val]);
this.numberArraySource.publish({modifier, payload: numberToInsert});
}
removeNumber(numberToRemove: number) {
let modifier = (full, val) => full.splice(full.indexOf(val),1);
this.numberArraySource.publish({modifier, payload: numberToRemove});
}
sort() {
let modifier = (full, val) => full.sort();
this.numberArraySource.publish({modifier});
}
reset() {
let modifier = (full, val) => INIT_STATE;
this.numberArraySource.publish({modifier});
}
}
Related
I have a service managing data fetched from a SocketIO API such as
export class DataService {
private data: SomeData[];
// ...
getData(): Observable<SomeData[]> {
// distinctUntilChanged was used to limit Observable to only emits when data changes
// but it does not seem to change much things...
return this.data.pipe(distinctUntilChanged());
}
// ...
}
and a component calling this service to do
this.banana$ = combineLatest([
someFnToRequestANetworkObject(),
DataService.getData()
]).pipe(
map(([network, data]) => network && data.some(_data=> _data.ip === network.ip))
);
The thing is that each time one of the Observable handled within combineLatest gets emitted, I get to call Array.prototype.some() function. Which I don't want to.
How could I optimize this code so that I don't call some too often ?
One thing to note about distinctUntilChanged() operator is it affects only the subscription. So the operators between the source observable and the subscription are still run. As workaround you could manually check if data has changed between emissions. Try the following
let oldData: any;
this.banana$ = combineLatest([
someFnToRequestANetworkObject(),
DataService.getData()
]).pipe(
map(([network, data]) => {
if (!oldData || oldData !== data) {
oldData = data;
return (network && data.some(_data=> _data.ip === network.ip));
}
return false; // <-- return what you wish when `data` hasn't changed
})
);
I think your hunch about distinctUntilChanged not working is correct.
By default, this operator uses an equality check to determine if two values are the same.
However, this will (in most cases) not work properly if the objects being compared aren't simple scalar values like numbers or booleans.
As it turns out, you can provide your own comparator function as the first argument to distinctUntilChanged.
This will be called on "successive" elements to determine if the new element is different from the most recent one.
The exact definition of this comparator function depends on what your SomeData class/interface looks like, along with what it means for an array of SomeData inhabitants to be the same as another one.
But, as an example, let's say that SomeData looks like this:
interface SomeData {
id: string;
name: string;
age: number;
}
and that SomeData inhabitants are the same if they have the same id.
Furthermore, let's suppose that two arrays of SomeDatas are the same if they contain exactly the same SomeData elements.
Then our comparator function might look like:
function eqSomeDataArrays(sds1: SomeData[], sds2: SomeData[]): boolean {
// Two arrays of `SomeData` inhabitants are the same if...
// ...they contain the same number of elements...
return sds1.length === sds2.length
// ...which are "element-wise", the same (i.e. have the same `id`)
&& sds1.every((sd1, i) => sd1.id === sds2[i].id)
}
To round it all out, your getData method would now look like:
getData(): Observable<SomeData[]> {
return this.data.pipe(distinctUntilChanged(eqSomeDataArrays));
}
in the pipe, after map call, you could use shareReplay(1)
this.banana$ = combineLatest([
someFnToRequestANetworkObject(),
DataService.getData()])
.pipe(
map(([network, data]) => network && data.some(_data=> _data.ip === network.ip)),
shareReplay(1)
);
I was trying to figure out RxJs. ShareReplay operator in particular. As per my understanding if there are two subscriptions to an observable then the observable should be executed twice. Unless there is a shareReplay involved. Obviously my understanding is incorrect because that is not what I is happening here. Could someone please help me understand this?
export class TestComponent implements OnInit {
constructor() {}
i = 0;
ngOnInit() {
console.clear();
let ob = this.httpcall().pipe(map(d => d));
let ob1$ = ob.pipe(map(d => d.toUpperCase()));
let ob2$ = ob.pipe(map(d => d.toLowerCase()));
ob1$.subscribe(d => {
console.log(d);
});
ob2$.subscribe(d => {
console.log(d);
});
}
httpcall() {
console.log("called");
this.i++;
return of("server cAlled:" + this.i);
}
}
Output:
called
SERVER CALLED:1
server called:1
The counter i did not get incremented to two even though there are two subscriptions and no shareReplay involved.
I was expecting(without shareReplay):
called
SERVER CALLED:1
called
server called:2
And with let ob = this.httpcall().pipe(map(d=>d),shareReplay()); I was expecting:
called
SERVER CALLED:1
server called:1
When you call subscribe, that will cause the observable to do everything it was defined to do. It was defined using of("server cAlled: 1");, which is then passed into a map operator. So since you subscribed twice, of will do its thing twice, and map will do its thing twice.
You happened to create the observable inside a function named httpcall, but the observable doesn't know anything about httpcall. httpcall will not be invoked an additional time.
If you want the incrementing of this.i to be part of what happens when subscribing, then you may need to use Observable.create. For example:
httpcall() {
return Observable.create((observer) => {
this.i++;
observer.next("server called: " + this.i);
observer.complete();
})
}
it seems that of return observable of it's own, so it's body doesn't get executed twice, so i recreate the example to show the difference between normal observable and of
export class AppComponent implements OnInit {
constructor(
) {
}
i = 0;
ngOnInit() {
console.clear();
const ob = this.httpCall1();
const ob1$ = ob.pipe(map(d => d.toUpperCase()));
const ob2$ = ob.pipe(map(d => d.toLowerCase()));
ob1$.subscribe(d => {
console.log(d);
});
ob2$.subscribe(d => {
console.log(d);
});
this.i = 0;
const ob2 = this.httpCall2();
const ob3$ = ob2.pipe(map(d => d.toUpperCase()));
const ob4$ = ob2.pipe(map(d => d.toLowerCase()));
ob3$.subscribe(d => {
console.log(d);
});
ob4$.subscribe(d => {
console.log(d);
});
}
httpCall1() {
return of('server1 called:' + ++this.i).pipe(
tap(d => console.log('tap1', d)),
);
}
httpCall2() {
return new Observable<string>((observer) => {
this.i++;
observer.next('server2 called: ' + this.i);
observer.complete();
}).pipe(
tap(d => console.log('tap2', d)),
);
}
}
That is because httpcall() is called only once via following line let ob = this.httpcall().pipe(map(d => d));. So it means that Observable that is returned by httpcall() will be reused thereafter.
If thats clear, now think about ob, which is source Observable. If you subscribe to this Observable you are going to get of("server cAlled:" + this.i); where this.i = 1. this.i is only going to increase if method httpcall() is executed once more, but it is not. Instead you are subscribing to the cold Observable ob which will just print what was used to create it. The value of this.i (which equals to 1) is already stored inside Observable ob and it is not going to be changed until new instance is created, no matter how many times you subscribe to ob.
Now lets look at the following code which is different:
let ob1$ = this.httpcall().pipe(map(d => d)).pipe(map(d => d.toUpperCase()));
let ob2$ = this.httpcall().pipe(map(d => d)).pipe(map(d => d.toLowerCase()));
In this situation httpcall() is called twice, thus this.i++; would happen twice and you would get what you were thinking to get.
I am trying to copy a state object:
#boundMethod
private _onClickDeleteAttachment(attachmentName: string): void {
console.log("_onClickDeleteAttachment | this.state.requestApproval[strings.Attachments]: ", this.state.requestApproval[strings.Attachments]);
let requestApprovalClone = {... this.state.requestApproval}
if (requestApprovalClone === this.state.requestApproval) {
console.log("they are ===");
}
else {
console.log(" they are not ===");
}
_.remove(requestApprovalClone[strings.Attachments], (attachment: any) => {
return attachment.FileName === attachmentName;
})
console.log("_onClickDeleteAttachment | this.state.requestApproval[strings.Attachments]: ", this.state.requestApproval[strings.Attachments]);
console.log("_onClickDeleteAttachment | requestApprovalClone[strings.Attachments]: ", requestApprovalClone[strings.Attachments]);
}
The state object is being altered too. From what I have read, I shouldn't mutate a state object but only change it with setState.
How can I correct this?
You are getting that behavior, because the
let requestApprovalClone = {... this.state.requestApproval}
is only shallow copying the data, your attachments property has some nested objects and it keeps the same reference and therefore when changing it, the cloned object gets altered and the state too.
To avoid that, you can perform another copy of your attachments property like this :
let attachments = [...requestApprovalClone[strings.Attachments]];
_.remove(attachments, function (attachment) {
return attachment.FileName === attachmentName;
});
Changing the attachments variable content won't afftect the state anymore.
you can read more about that behavior here
It has to to with the way that JS handles their const references.
For those who feel adventerous:
let requestApprovalClone = JSON.parse(JSON.stringify(this.state.requestApproval));
// modify requestApprovalClone ...
this.setState({
requestApproval:requestApprovalClone
})
It would be interesting if Object.assign({},this.state.requestApproval); is faster than the whole JSON stringify/parse stuff or vice versa
You can have something like:
let requestApprovalClone = Object.assign({},this.state.requestApproval);
requestApprovalClone.strings.Attachments = requestApprovalClone.strings.Attachments.slice(); // will create a shallow copy of this array as well
_.remove(requestApprovalClone[strings.Attachments], (attachment: any) => {
return attachment.FileName === attachmentName;
})
this.setState({
requestApproval:requestApprovalClone
})// If you want to update that in state back
Is there a good way to check if not completed Observable is empty at that exact time?
let cache = new ReplaySubject<number>(1);
...
// Here I want to know if 'cache' still empty or not. And, for example, fill it with initial value.
cache.isEmpty().subscribe(isEmpty => {
if (isEmpty) {
console.log("I want to be here!!!");
cache.next(0);
}
});
// but that code does not work until cache.complete()
Actually, it's not that simple and the accepted answer is not very universal. You want to check whether ReplaySubject is empty at this particular point in time.
However, if you want to make this truly compatible with ReplaySubject you need to take into account also windowTime parameter that specifies "time to live" for each value that goes through this object. This means that whether your cache is empty or not will change in time.
ReplaySubject has method _trimBufferThenGetEvents that does what you need. Unfortunately, this method is private so you need to make a little "hack" in JavaScript and extend its prototype directly.
import { ReplaySubject } from 'rxjs';
// Tell the compiler there's a isNowEmpty() method
declare module "rxjs/ReplaySubject" {
interface ReplaySubject<T> {
isNowEmpty(): boolean;
}
}
ReplaySubject.prototype['isNowEmpty'] = function() {
let events = this._trimBufferThenGetEvents();
return events.length > 0;
};
Then using this ReplaySubject is simple:
let s = new ReplaySubject<number>(1, 100);
s.next(3);
console.log(s.isNowEmpty());
s.next(4);
setTimeout(() => {
s.next(5);
s.subscribe(val => console.log('cached:', val));
console.log(s.isNowEmpty());
}, 200);
setTimeout(() => {
console.log(s.isNowEmpty());
}, 400);
Note that some calls to isNowEmpty() return true, while others return false. For example the last one returns false because the value was invalidated in the meantime.
This example prints:
true
cached: 5
true
false
See live demo: https://jsbin.com/sutaka/3/edit?js,console
You could use .scan() to accumulate your count, and map that to a boolean whether it's nonzero. (It takes a second parameter for a seed value which would make it start with a 0, so it always reflects the current count.)
I've also added a .filter() instead of an if statement to make it cleaner:
let cache = new ReplaySubject<number>(1);
cache
.map((object: T) => 1)
.scan((count: number, incoming: number) => count + incoming, 0)
.map((sum) => sum == 0)
.filter((isEmpty: boolean) => isEmpty)
.subscribe((isEmpty: boolean) => {
console.log("I want to be here!!!");
cache.next(0);
});
You could use takeUntil():
Observable.of(true)
.takeUntil(cache)
.do(isEmpty => {
if (isEmpty) {
console.log("I want to be here!!!");
cache.next(0);
}
})
.subscribe();
However this will just work once.
Another way would be to "null" the cache and initialize it as empty by using a BehaviorSubject:
let cache = new BehaviorSubject<number>(null as any);
...
cache
.do(content => {
if (content == null) {
console.log("I want to be here!!!");
cache.next(0);
}
})
.subscribe();
And of course you could initialize the cache with some default value right away.
startWith
let cache = new ReplaySubject<number>(1);
isEmpty$ = cache.pipe(mapTo(false), startWith(true));
This says:
Whatever the value is emitted by cache - map it to false. (because it isn't empty after an emission)
Start with true if nothing has been emitted yet (because that means it's empty)
How do I wait for one stream (say, StreamA) to return non-null value and then invoke StreamB subscribe function. I'm not particularly interested in StreamA's value. In turn, I am trying to get StreamB's value which might have been updated before the StreamA returned non-null value, and might not have any new events.
I tried, pausable, by looking at this: RxJS: How can I do an "if" with Observables?, but unfortunately could not get it to work. This is because, there are no exported class pausable, rxjs v 5.0.0-beta.6.
This is how far, I've come up with, as per the answer.
export class AuthService {
userModel: FirebaseListObservable = this.af.database.list(/users);
constructor(private af: AngularFire) {
var user = this.currentAuthor();
var userStream = user;
this.af.auth.flatMap((d) => { console.log(d);return this.userModel.publishReplay(1); });
this.userModel
.subscribe((data) => {
var flag = false;
data.forEach((item) => {
if (item.$key && item.$key === user.uid) {
flag = true;
return;
}
});
if (flag) {
console.log('hello');
} else {
this.userModel.push({
firstName: user.auth.displayName.substr(0, user.auth.displayName.lastIndexOf(' ')),
lastName: user.auth.displayName.substr(user.auth.displayName.lastIndexOf(' '), user.auth.displayName.length),
displayPic: user.auth.photoURL,
provider: user.provider,
uid: user.uid
}
);
}
})
}
public currentAuthor():FirebaseAuthState {
return this.af.auth.getAuth();
}
Hope, I can make myself clear. Even I am getting confused now. :p.
I am new to rxjs and reactive programming. And, any help will be appreciated.
And, btw, thanks for stopping by. :)
I suppose by plausible you mean pausable? I am not sure what exactly you are trying to achieve here (control flow?). However, if you want streamB value after streamA produces a value, then you can use flatMap.
streamA.flatMapLatest(function (_){return streamB})
That should give you, anytime streamA emits, the values emitted after that time by streamB.
If you want values including the last one B emitted prior to that time, you can use streamBB = streamB.publishReplay(1) and
streamA.flatMapLatest(function (_){return streamBB})
Haven't tested it, so keep me updated if that works.