RXJS: Subscription to share operator causes strange behaviour - rxjs

Using these RxJS tools: BehaviorSubject, Subscribe and Next
Please refer to the this codesandbox, and look at the console to see a visual: https://codesandbox.io/s/fancy-bird-0m81p
You will notice the object "C" value in the subscription is "1 stream behind
Consider the the following code:
const initialState = { a: 1, b: 2, c: 3 };
const Store$ = new BehaviorSubject(initialState);
const StoreUpdates$ = Store$.pipe(
scan((acc, curr) => {
return Object.assign({}, acc, curr);
}, initialState),
share()
);
export const updateStore = update => {
Store$.next(update);
};
StoreUpdates$.pipe(
distinctUntilChanged((p, n) => {
return p.b === n.b;
})
).subscribe(store => {
Store$.next({ c: Math.random() });
});
StoreUpdates$.subscribe(store => {
console.log("Subscription Check:: Notice issue here", store);
});
When you call the updateStore function, in the console.log you will notice that the C value, which is updated in a next call within a subscription, appears in the first iteration and the older value appears in the last iteration. So somehow it looks the next.call within the subscription happens "Before"
I believe the codesandox will illustrate and make it more clear.
How do I maintain the correct order of events so that the latest update appears last in the subscription?

For the moment a non-ideal way of solving this issue is to wrap the next statement within the subscribe in a timeout. If anyone has a better solution please do post.
StoreUpdates$.pipe(
distinctUntilChanged((p, n) => {
return p.b === n.b;
})
).subscribe(store => {
setTimeout(() => {
Store$.next({ c: Math.random() });
},0)
});

Related

Delay batch of observables with RxJS

I perform http requests to my db and have noticed that if I send all the requests at once, some of them will get a timeout errors. I'd like to add a delay between calls so the server doesn't get overloaded. I'm trying to find the RxJS solution to this problem and don't want to add a setTimeout.
Here is what I currently do:
let observables = [];
for(let int = 0; int < 10000; int++){
observables.push(new Observable((observer) => {
db.add(doc[int], (err, result)=>{
observer.next();
observer.complete();
})
}))
}
forkJoin(observables).subscribe(
data => {
},
error => {
console.log(error);
},
() => {
db.close();
}
);
You can indeed achieve this with Rxjs quite nicely. You'll need higher order observables, which means you'll emit an observable into an observable, and the higher order observable will flatten this out for you.
The nice thing about this approach is that you can easily run X requests in // without having to manage the pool of requests yourself.
Here's the working code:
import { Observable, Subject } from "rxjs";
import { mergeAll, take, tap } from "rxjs/operators";
// this is just a mock to demonstrate how it'd behave if the API was
// taking 2s to reply for a call
const mockDbAddHtppCall = (id, cb) =>
setTimeout(() => {
cb(null, `some result for call "${id}"`);
}, 2000);
// I have no idea what your response type looks like so I'm assigning
// any but of course you should have your own type instead of this
type YourRequestType = any;
const NUMBER_OF_ITEMS_TO_FETCH = 10;
const calls$$ = new Subject<Observable<YourRequestType>>();
calls$$
.pipe(
mergeAll(3),
take(NUMBER_OF_ITEMS_TO_FETCH),
tap({ complete: () => console.log(`All calls are done`) })
)
.subscribe(console.log);
for (let id = 0; id < NUMBER_OF_ITEMS_TO_FETCH; id++) {
calls$$.next(
new Observable(observer => {
console.log(`Starting a request for ID "${id}""`);
mockDbAddHtppCall(id, (err, result) => {
if (err) {
observer.error(err);
} else {
observer.next(result);
observer.complete();
}
});
})
);
}
And a live demo on Stackblitz: https://stackblitz.com/edit/rxjs-z1x5m9
Please open the console of your browser and note that the console log showing when a call is being triggered starts straight away for 3 of them, and then wait for 1 to finish before picking up another one.
Looks like you could use an initial timer to trigger the http calls. e.g.
timer(delayTime).pipe(combineLatest(()=>sendHttpRequest()));
This would only trigger the sendHttpRequest() method after the timer observable had completed.
So with your solution. You could do the following...
observables.push(
timer(delay + int).pipe(combineLatest(new Observable((observer) => {
db.add(doc[int], (err, result)=>{
observer.next();
observer.complete();
}))
}))
Where delay could probably start off at 0 and you could increase it using the int index of your loop by some margin.
Timer docs: https://www.learnrxjs.io/learn-rxjs/operators/creation/timer
Combine latest docs: https://www.learnrxjs.io/learn-rxjs/operators/combination/combinelatest
merge with concurrent value:
mergeAll and mergeMap both allow you to define the max number of subscribed observables. mergeAll(1)/mergeMap(LAMBDA, 1) is basically concatAll()/concatMap(LAMBDA).
merge is basically just the static mergeAll
Here's how you might use that:
let observables = [...Array(10000).keys()].map(intV =>
new Observable(observer => {
db.add(doc[intV], (err, result) => {
observer.next();
observer.complete();
});
})
);
const MAX_CONCURRENT_REQUESTS = 10;
merge(...observables, MAX_CONCURRENT_REQUESTS).subscribe({
next: data => {},
error: err => console.log(err),
complete: () => db.close()
});
Of note: This doesn't batch your calls, but it should solve the problem described and it may be a bit faster than batching as well.
mergeMap with concurrent value:
Perhaps a slightly more RxJS way using range and mergeMap
const MAX_CONCURRENT_REQUESTS = 10;
range(0, 10000).pipe(
mergeMap(intV =>
new Observable(observer => {
db.add(doc[intV], (err, result) => {
observer.next();
observer.complete();
});
}),
MAX_CONCURRENT_REQUESTS
)
).subscribe({
next: data => {},
error: err => console.log(err),
complete: () => db.close()
});

Cypress spy working inconsistently between tests

I am spying on calls to fillText in a canvas. My component updates every 1000ms. Here's how I set things up:
In before, I send data that renders the canvas.
In beforeEach, I get the context and set the spy to the textSpy variable, which is captured by all the convenience methods.
In the test, I call convenience methods which:
send some new data (let's say it's a Series with the series number 1)
wait until the canvas has drawn (by waiting for "Series 1" which is also rendered as html)
assert that the spy has been called with text corresponding to the length and id number of the series.
The convenience methods are maybe a little all over the place/hard to follow so I hope my explanation helped:
describe('Displaying series after HScroll updates', () => {
const sendSeriesUpTo = (len: number, interval = 0.02) => {
// creates a series of the given length with values the given interval apart
// and sends that series
};
before(setupNonReloadingTests);
after(tearDownNonReloadingTests);
describe('data updates', () => {
let textSpy: SinonSpy;
const getSpy = () => cy.context('series').then(context => textSpy = cy.spy(context, 'fillText'));
// pass in an array of strings that must all have been called, but don't have to be the only calls
const expectTextCall = (textArray: string[]) => {
const textCalls = textSpy.getCalls().map(c => c.args[0]);
textArray.forEach(text => expect(textCalls).to.include(text));
};
const sendAndExpectFrameCount = (frameCount: number, series: string[] = ['1'], allSeries: string = '1') => {
sendSeriesUpTo(frameCount);
cy.contains(`Series: ${ allSeries }`).then(() => {
const arr = [`(${ frameCount } frames)`];
series.forEach(s => arr.push(`SERIES ${ s }`));
expectTextCall(arr);
});
};
const sendAndExpectRange = (from: number, to: number) => {
const diff = to - from + 1;
const array = [...Array(diff).keys()];
array.forEach(n => {
sendAndExpectFrameCount(n + from);
});
};
describe('receiving a single non-overflowing series', () => {
before(() => {
cy.sendStudyUpdateWithSeriesArray([1, 2, 3].map(n => createFdsObject()));
});
beforeEach(() => {
cy.context('series').then(context => textSpy = cy.spy(context, 'fillText'));
});
describe('before overflowing', () => {
it('displays the initial series, ' +
'displays non-overflowing updates to the series', () => {
sendAndExpectRange(80, 85);
});
});
describe('overflowing', () => {
it('scrolls to the end (most current) when a frame is added to overflow the screen', () => {
sendAndExpectFrameCount(90)
});
Okay, so the problem is that the first test passes, but the second test says the spy hasn't had any calls.
I think Cypress resets the spy by default in between tests, but I also re-set the spy in my beforeEach.
The spy sees the calls in the first test but not in the second test. The browser doesn't reload in between tests, so the spy is definitely using the same canvas context every time.
What is wrong here!?

rxjs subscription being called more often than expected

I have a BehaviorSubject stream of functions. I have an initialState object represented as an immutable Record. Those functions are scanned and used to manipulate the state. The code looks like this:
const initialState = Record({
todo: Record({
title: "",
}),
todos: List([Record({title: "first todo"})()])
})
const actionCreator = (update) => ({
addTodo(title) {
update.next((state) => {
console.log({title}); // for debugging reasons
const todo = Record({title})()
return state.set("todos", state.get("todos").push(todo))
})
},
typeNewTodoTitle(title) {
update.next((state) => state.set("todo", state.get("todo").set("title", title))
})
})
const update$ = new BehaviorSubject(state => state);
const actions = actionCreator(update$);
const state = update$.pipe(
scan(
(state, updater) => updater(state), initialState()
),
// share() without share weird things happen
)
I have a very simple test written for this
it("should only respond to and call actions once", () => {
const subscripition = chai.spy();
const addTodo = chai.spy.on(actions, 'addTodo');
const typeNewTodoTitle = chai.spy.on(actions, 'typeNewTodoTitle');
state
.pipe(
map(s => s.get("todo")),
distinctUntilChanged()
)
.subscribe(subscripition);
state
.pipe(
map(s => s.get("todos")),
distinctUntilChanged()
)
.subscribe(subscripition);
actions.addTodo('test');
expect(subscripition).to.have.been.called.twice // error
actions.typeNewTodoTitle('test');
expect(subscripition).to.have.been.called.exactly(3) // error
expect(addTodo).to.have.been.called.once
expect(typeNewTodoTitle).to.have.been.called.once
});
});
The first strange behavior is that subscription has been called 3 times and then 4 instead of 2 and then 3 times. The second strange behavior is that even though each action has only been called once, the console.log has been called twice. I can fix this problem by adding share() to the pipeline, but I can't figure out why that's required.

How can I reset accumulator of scan on ReplaySubject?

I have a ReplaySubject that accumulate data with scan operator and every 10000 ms should be reset. Is there any another way to do it?
Now:
let subject = new ReplaySubject();
subject.scan((acc, cur) => {
acc.push(cur);
return acc;
}, [])
.subscribe(events => {
localStorage.setItem('data', JSON.stringify(events))
});
subject
.bufferTime(10000)
.map(() => {
subject.observers[0]._seed = [];
})
.subscribe(() => localStorage.removeItem('data'));
I asked a very similar question few days ago and later answered myself
accumulating values such as with scan but with the possibility to reset the accumulator over time
maybe this can help you
SOME MORE DETAILS
An alternative approach is to have an Observable which acts as a timer which emits at a fixed interval, 10000ms in your case.
Once this timer emits, you pass the control to the Observable that cumululates via scan operator. To pass the control you use the switchMap operator to make sure the previous instance of the Observable completes.
If I understand correctly what you want to achieve, I would use a normal Subject rather than ReplaySubject.
The code could look something like this
const subject = new Subject<number>();
const timer = Observable.timer(0, 1000).take(4);
const obs = timer.switchMap(
() => {
console.log('-----');
return subject
.scan((acc, cur) => {
acc.push(cur);
return acc;
}, []);
}
)
obs.subscribe(
events => {
console.log(JSON.stringify(events))
}
);
// TEST DATA EMITTED BY THE SUBJECT
setTimeout(() => {
subject.next(1);
}, 100);
setTimeout(() => {
subject.next(2);
}, 1100);
setTimeout(() => {
subject.next(3);
}, 2100);
setTimeout(() => {
subject.next(4);
}, 2200);

RxJS emit array items over time?

I'm trying to emit simple array values one after another with 500ms in between:
var a = Rx.Observable.from([1,2,3]);
a.interval(500).subscribe(function(b) { console.log(b); });
However, this throws an exception:
Uncaught TypeError: a.interval is not a function.
Three ways to do it, with RxJS version 6 :
1. Using concatMap
import { from, of, pipe } from 'rxjs';
import { concatMap, delay } from 'rxjs/operators';
const array = [1, 2, 3, 4, 5];
from(array)
.pipe(
concatMap(val => of(val).pipe(delay(1000))),
)
.subscribe(console.log);
2. Using zip and interval
import { from, pipe, interval } from 'rxjs';
import { delay, zip} from 'rxjs/operators';
const array = [1, 2, 3, 4, 5];
from(array)
.pipe(
zip(interval(1000), (a, b) => a),
)
.subscribe(console.log);
3. Using interval as source
import { interval, pipe } from 'rxjs';
import { map, take } from 'rxjs/operators';
const array = [1, 2, 3, 4, 5];
interval(1000)
.pipe(
take(array.length),
map(i => array[i])
)
.subscribe(console.log);
As already pointed out by xgrommx, interval is not an instance member of an observable but rather a static member of Rx.Observable.
Rx.Observable.fromArray([1,2,3]).zip(
Rx.Observable.interval(500), function(a, b) { return a; })
.subscribe(
function(x) { document.write(x + '<br \>'); },
null,
function() { document.write("complete"); });
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/2.5.2/rx.all.min.js"></script>
This is how I would do it:
var fruits = ['apple', 'orange', 'banana', 'apple'];
var observable = Rx.Observable.interval(1000).take(fruits.length).map(t => fruits[t]);
observable.subscribe(t => {
console.log(t);
document.body.appendChild(document.createTextNode(t + ', '));
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/2.5.2/rx.all.min.js"></script>
var arrayList = [1,2,3,4,5];
var source = Rx.Observable
.interval(500/* ms */)
.timeInterval()
.take(arrayList.length);
source.subscribe(function(idx){
console.log(arrayList[idx]);
//or document.write or whatever needed
});
Pretty late but a simpler solution would be :
const arr = ["Hi,", "how", "may", "I", "help", "you?"];
Rx.Observable.interval(500)
.takeWhile(_ => _ < arr.length)
.map(_ => arr[_])
.subscribe(_ => console.log(_))
I find Weichhold technique to be the best but that it would gain in clarity of intent by extracting the zipped value outside of the zip:
// assume some input stream of values:
var inputs = Obs.of(1.2, 2.3, 3.4, 4.5, 5.6, 6.7, 7.8);
// emit each value from stream at a given interval:
var events = Obs.zip(inputs, Obs.interval(1000))
.map(val => val[0])
.forEach(console.log);
If you want to release samples over time, you can do something like this
const observable = interval(100).pipe(
scan((acc, value) => [value, ...acc], []),
sampleTime(10000),
map((acc) => acc[0])
);
I had a little different requirement, my array kept updating over time too. So basically I had to implement a queue which I can dequeue at a regular interval, but I didn't want to use an Interval.
If somebody has a need for something like this then probably this solution can help:
I have a function createQueue() that takes the array as an input and returns an Observable which we subscribe for listening to events from the Array at a regular interval.
The function also modifies the 'push()' method of the passes array so that whenever any item is pushed in the array, the Observable would emit.
createQueue(queue: string[]) {
return Observable.create((obs: Observer<void>) => {
const arrayPush = queue.push;
queue.push = (data: string) => {
const returnVal = arrayPush.call(queue, data);
obs.next();
return returnVal;
}
}).pipe(switchMap(() => {
return from([...queue])
.pipe(
concatMap(val => of(val)
.pipe(delay(1000)))
);
}), tap(_ => queue.shift()))
}
Lets say that the array is: taskQueue = [];
So, we need to pass it to the above function and subscribe to it.
createQueue(taskQueue).subscribe((data) => {
console.log('Data from queue => ', data);
});
Now, every time we do taskQueue.push('<something here>'), the subscription will trigger after a delay of "1000ms".
Please note: we should not be assigning a new array to the taskQueue after createQueue() has been called, or else we will loose the modified push().
Here is a dummy example for the above implementation: Test Example
Rx.Observable instance doesn't have interval method http://xgrommx.github.io/rx-book/content/core_objects/observable/observable_instance_methods/index.html. You can use like this.
Rx.Observable.interval(500)
.map(function(v) { return [1,2,3];})
.subscribe(console.log.bind(console));

Resources