Is-there an opposite of the `race` operator in RxJS? - javascript

I have two observables and I want listen to the one that emits its first value last, is there an operator for this ? Something like that :
let obs1 = Rx.Observable.timer(500,500);
let obs2 = Rx.Observable.timer(1000,1000); // I want the values from this one
let sloth = Rx.Observable.sloth(obs1,obs2);
where the sloth observable would emit the values from obs2 as it is the one who emits its first value last.
If that's not the case, is there any other way ?

I see this possibility, for now, but I'm curious if someone find anything else :
let obs1 = Rx.Observable.timer(500,500).map(i=>`cheetah ${i}`);
let obs2 = Rx.Observable.timer(1000,1000).map(i=>`sloth ${i}`);
let sloth = Rx.Observable.merge(
obs1.take(1).mapTo(obs1),
obs2.take(1).mapTo(obs2)
).takeLast(1).mergeAll()
sloth.subscribe(data=>console.log(data))
<script src="https://unpkg.com/#reactivex/rxjs#5.3.0/dist/global/Rx.js"></script>
Edit as pointed out by #user3743222 (very nice nickname :-D ), it would not work for hot observables, but this should be fine :
let obs1 = Rx.Observable.timer(500,500).map(i=>`cheetah ${i}`).publish();
let obs2 = Rx.Observable.timer(1000,1000).map(i=>`sloth ${i}`).publish();
obs1.connect();
obs2.connect();
let sloth = Rx.Observable.merge(
obs1.take(1).map((val)=>obs1.startWith(val)),
obs2.take(1).map((val)=>obs2.startWith(val))
).takeLast(1).mergeAll();
sloth.subscribe(data=>console.log(data));
<script src="https://unpkg.com/#reactivex/rxjs#5.3.0/dist/global/Rx.js"></script>

I like your solution (though I suspect you might never see the first emitted value if you have a hot stream - if the source is cold, all seems good). Can you make a jsfiddle to check that out? If you dont miss any value, your solution is the best. If you do, it might be possible to correct it by adding the skipped value back to the source (obs1.take(1).map(val => obs1.startWith(val)).
Otherwise, for a generic lengthy solution, the key here is that you have state, so you need also the scan operator. We tag the source with an index, and we keep a state which represents the indices of the sources which already have started. When all but one have started, we know the index of the one who hasnt, and we pick only the values from that one. Please note, that this should work independently of whether the sources are hot or cold as all is made in one pass, i,e, there is no multiple subscriptions.
Rx.Observable.merge(
obs1.map(val => {val, sourceId: 1})
obs2.map(val => {val, sourceId: 2})
obsn.map(val => {val, sourceId: n})
).scan(
(acc, valueStruct) => {
acc.valueStruct = valueStruct
acc.alreadyEmitted[valueStruct.sourceId - 1] = true
if (acc.alreadyEmitted.filter(Boolean).length === n - 1) {
acc.lastSourceId = 1 + acc.alreadyEmitted.findIndex(element => element === false)
}
return acc
}, {alreadyEmitted : new Array(n).fill(false), lastSourceId : 0, valueStruct: null}
)
.map (acc => acc.valueStruct.sourceId === acc.lastSourceId ? acc.valueStruct.val : null )
.filter(Boolean)
Maybe there is shorter, I dont know. I'll try to put that in a fiddle to see if it actually works, or if you do before let me know.

How about this?
let obs1 = Rx.Observable.timer(500,500);
let obs2 = Rx.Observable.timer(1000,1000);
let sloth = Rx.Observable.race(
obs1.take(1).concat(obs2),
obs2.take(1).concat(obs1)
).skip(1);
And as a function with multiple Observables support:
let sloth = (...observables) =>
observables.length === 1 ?
observables[0] :
observables.length === 2 ?
Rx.Observable.race(
observables[0].take(1).concat(observables[1]),
observables[1].take(1).concat(observables[0])
).skip(1) :
observables.reduce((prev, current) => sloth(prev, current))[0];

I had the same issue and was able to solve it using a combination of merge and skipUntil. The pipe(last()) stops you receiving multiple results if both complete at the same time.
Try pasting the following into https://rxviz.com/:
const { timer, merge } = Rx;
const { mapTo, skipUntil, last } = RxOperators;
let obs1 = timer(500).pipe(mapTo('1'));
let obs2 = timer(1000).pipe(mapTo('2')); // I want the values from this one
let sloth = merge(
obs1.pipe(skipUntil(obs2)),
obs2.pipe(skipUntil(obs1))
).pipe(last())
sloth

Using RxJS 6 and ReplaySubject:
function lastOf(...observables) {
const replayable = observables
.map(o => {
let r = o.pipe(multicast(new ReplaySubject(1)));
r.connect();
return r;
});
const racing = replayable
.map((v, i) => v.pipe(
take(1),
mapTo(i),
))
;
return of(...racing).pipe(
mergeAll(),
reduce((_, val) => val),
switchMap(i => replayable[i]),
);
}
Use:
const fast = interval(500);
const medium = interval(1000);
const slow = interval(2000);
lastOf(fast, slow, medium).subscribe(console.log);

Related

React hooks removing item from an array and add it to another array

export default function ShoppingCart() {
const classes = useStyle();
const {
productsList, filteredProductsList, setFilteredProductsList, setProductsList,
} = useContext(productsContext);
const [awaitingPaymentList, setAwaitingPaymentList] = useState([]);
const [addedToCartList, setAddedToCartList] = useState([]);
const addToCartHandler = useCallback((itemId) => {
const awaitingPaymentListIds = awaitingPaymentList.map((item) => item.id);
const isInAwaitingPaymentList = awaitingPaymentListIds.includes(itemId);
isInAwaitingPaymentList ? setAddedToCartList([...addedToCartList, addedToCartList.push(awaitingPaymentList[awaitingPaymentList.findIndex((item) => item.id === itemId)])]) : setAddedToCartList([...addedToCartList]);
isInAwaitingPaymentList
? setAwaitingPaymentList(awaitingPaymentList.splice(awaitingPaymentList.findIndex((item) => item.id === itemId), 1))
: setAwaitingPaymentList([...awaitingPaymentList ])
setProductsList(awaitingPaymentList);
}, [addedToCartList, awaitingPaymentList, setProductsList]);
useEffect(() => {
setFilteredProductsList(
productsList.filter((product) => product.status === 'AWAITING_PAYMENT'),
);
}, [productsList, setFilteredProductsList, setFilteredProductsList.length]);
useEffect(() => {
setAwaitingPaymentList(filteredProductsList);
}, [filteredProductsList]);
I manage to delete the item from awaitingPaymentList and to add it into addedToCartList but looks like I am doing something wrong because it is adding the object, but the previous ones are replaced with numbers :). On the first click, the array is with one object inside with all data, but after each followed click is something like this => [1,2,3, {}].
When I console log addedToCartList outside addToCartHandler function it is showing an array: [1] :)))
Since there is some code I hope I am not going to receive a lot of negative comments like last time. And if it's possible, to give me a clue how to make it for all items to be transferred at once, because there will be a button to add all. Thank you for your time.
I think this line of code is causing issue:
isInAwaitingPaymentList
? setAddedToCartList([
...addedToCartList,
addedToCartList.push(
awaitingPaymentList[
awaitingPaymentList.findIndex((item) => item.id === itemId)
]
)
])
: setAddedToCartList([...addedToCartList]);
array.prototype.push returns the new length of the array that you are pushing into, this is likely where the incrementing element values are coming from. The push is also a state mutation.
It is not really clear what you want this code to do, but I think the push is unnecessary. Perhaps you meant to just append the last element into the new array you are building.
isInAwaitingPaymentList
? setAddedToCartList([
...addedToCartList, // <-- copy state
awaitingPaymentList[ // <-- add new element at end
awaitingPaymentList.findIndex((item) => item.id === itemId)
]
])
: setAddedToCartList([...addedToCartList]);
Suggestion
If you simply want to move an element from one array to another then find it in the first, then filter it from the first, and copy to the second if it was found & filtered.
const itemToMove = awaitingPaymentList.find(item => item.id === itemId);
setAwaitingPaymentList(list => list.filter(item => item.id !== itemId));
itemToMove && setAddedToCartList(list => [...list, { ...itemToMove }])

Wild card filtering

I am filtering on the cached query result to see if it has the search value.
return this.cachedResults.filter(f => f.name.toLowerCase().indexOf(this.searchValue.toLowerCase()) !== -1);
This works great if the searchvalue is exactly same as the f.name. I want to filter even if it has the partial value. Like a wild card filtering. How can I do that here?
What you're doing will match partially as well in case when f.name includes whole of searchValue doesn't matter at what position.
What you might also want is, it should match even when searchValue includes whole of f.name and not just the other way around.
return this.cachedResults.filter(f => {
return f.name.toLowerCase().indexOf(this.searchValue.toLowerCase()) !== -1)
|| this.searchValue.toLowerCase().indexOf(f.name.toLowerCase()) !== -1)
}
Also consider checking out String.prototype.includes()
Try to use regexp (below support for ? and * wildcards)
let cachedResults = ['abcdef', 'abc', 'xyz', 'adcf', 'abefg' ];
let wild = 'a?c*';
let re = new RegExp('^'+wild.replace(/\*/g,'.*').replace(/\?/g,'.')+'$');
let result = cachedResults.filter( x => re.test(x.toLowerCase()) );
console.log(result);

RxJS: Debounce with regular sampling

I have an Observable that emits a stream of values from user input (offset values of a slider).
I want to debounce that stream, so while the user is busy sliding, I only emit a value if nothing has come through for, say 100ms, to avoid being flooded with values. But then I also want to emit a value every 1 second if it is just endlessly debouncing (user is sliding back and forth continuously). Once the user stops sliding though, I just want the final value from the debounced stream.
So I want to combine the debounce with a regular "sampling" of the stream. Right now my setup is something like this:
const debounce$ = slider$.debounceTime(100),
sampler$ = slider$.auditTime(1000);
debounce$
.merge(sampler$)
.subscribe((value) => console.log(value));
Assuming the user moves the slider for 2.4 seconds, this emits values as follows:
start end
(x)---------|---------|---(x)|----|
| | | |
1.0 2.0 2.5 3.0 <-- unwanted value at the end
^ ^ ^
sample sample debounce <-- these are all good
I don't want that extra value emitted at 3 seconds (from the sampler$ stream).
Obviously merge is the wrong way to combine these two streams, but I can't figure out what combination of switch, race, window or whatever to use here.
You can solve the problem by composing an observable that serves as a signal, indicating whether or not the user is currently sliding. This should do it:
const sliding$ = slider$.mapTo(true).merge(debounce$.mapTo(false));
And you can use that to control whether or not the sampler$ emits a value.
A working example:
const since = Date.now();
const slider$ = new Rx.Subject();
const debounce$ = slider$.debounceTime(100);
const sliding$ = slider$.mapTo(true).merge(debounce$.mapTo(false));
const sampler$ = slider$
.auditTime(1000)
.withLatestFrom(sliding$)
.filter(([value, sliding]) => sliding)
.map(([value]) => value);
debounce$
.merge(sampler$)
.subscribe(value => console.log(`${time()}: ${value}`));
// Simulate sliding:
let value = 0;
for (let i = 0; i <= 2400; i += 10) {
value += Math.random() > 0.5 ? 1 : -1;
slide(value, i);
}
function slide(value, at) {
setTimeout(() => slider$.next(value), at);
}
function time() {
return `T+${((Date.now() - since) / 1000).toFixed(3)}`;
}
.as-console-wrapper { max-height: 100% !important; top: 0; }
<script src="https://unpkg.com/rxjs#5/bundles/Rx.min.js"></script>
For those who are interested, this is the approach I took, inspired by #cartant's answer.
const slider$ = new Rx.Subject();
const nothing$ = Rx.Observable.never();
const debounce$ = slider$.debounceTime(100);
const sliding$ = slider$.mapTo(true)
.merge(debounce$.mapTo(false))
.distinctUntilChanged();
const sampler$ = sliding$
.switchMap((active) => active ? slider$.auditTime(1000) : nothing$);
debounce$
.merge(sampler$)
.subscribe(value => console.log(`${time()}: ${value}`));
The difference is adding distinctUntilChanged on the sliding$ stream to only get the on/off changes, and then doing a switchMap on that to either have the sampler return values or not.

Clear an Rx.Observable bufferCount with an event driven Timeout?

I'm using rxjs 5.0:
How can I set a timeout, on this buffer. So that it will clear the bufferCount (11) when no keyup events happen for 5 seconds?
var keys = Rx.Observable.fromEvent(document, 'keyup');
var buffered = keys.bufferCount(11,1);
buffered.subscribe(x => console.log(x));
You can append a timeoutWith, which could return a fresh buffered after a certain timeout (5seconds in your case).
const keys$ = Rx.Observable.fromEvent(document, "keyup")
.map(ev => ev.keyCode|| ev.which); // this is just to have a readable output here in the SO-console
const buffered$ = keys$
.bufferCount(3,1) // replaced your 11 with 3 for easy demonstration
.timeoutWith(2000, Rx.Observable.defer(() => { // replaced 5 with 2 seconds (easier to test here)
console.log("New Buffer!");
return buffered$;
}));
buffered$.subscribe(console.log);
<script src="https://unpkg.com/rxjs/bundles/Rx.min.js"></script>
As an improvement, this could be even enhanced to only start the stream on the first stroke, otherwise we would have a constant timeout running (not critical, but could still be prevented).
const keys$ = Rx.Observable.fromEvent(document, "keyup")
.map(ev => ev.keyCode|| ev.which); // this is just to have a readable output here in the SO-console
const buffered$ = keys$
.take(1)
.switchMap(firstKey => {
console.log("New Buffer!");
return keys$
.startWith(firstKey)
.bufferCount(3,1) // replaced your 11 with 3 for easy demonstration
.timeoutWith(2000, Rx.Observable.defer(() => buffered$)); // replaced 5 with 2 seconds (easier to test here)
});
buffered$.subscribe(console.log);
<script src="https://unpkg.com/rxjs/bundles/Rx.min.js"></script>
I have another (and probably easier to understand) solution using window and switchMap():
var keys = Rx.Observable.fromEvent(document.getElementById('myinput'), 'keyup')
.map(event => event.keyCode)
.share();
var buffered = keys
.window(keys.debounceTime(5000))
.switchMap(observable => observable.bufferCount(5, 1))
.filter(buffer => buffer.length === 5);
buffered.subscribe(x => console.log(x));
See demo: https://jsbin.com/cakoru/17/edit?js,console,output
When you don't type for at least 5s the window() operator creates a new Observable that is subscribed internally in switchMap() and chained with a new .bufferCount() operator.
Here's how I'd do it:
const keys$ = Rx.Observable.fromEvent(document, 'keyup').map(ev => ev.keyCode|| ev.which);
keys$
.debounceTime(5000)
.startWith({})
.switchMap(x => keys$.bufferCount(11, 1))
.subscribe(x => console.log(x));
Here we've got a stream that yields a value each time typing stops for five seconds (kicked off with a dummy value) that switchMaps into a bufferCount.

Cyclejs and trigger events with values

I m trying to learn cyclejs and reactive programming, and I can't get how to manage events with values.
For example,I need to create four functions that makes some maths operations such as :
addition
substraction
divison
multiplication
Here's the code that I have :
function main ({DOM}) {
const secondNumber$ = DOM
.select('.second-number')
.events('change')
.map(ev => ev.target.value)
.startWith(0)
const firstNumber$ = DOM
.select('.first-number')
.events('change')
.map(ev => ev.target.value)
.startWith(0)
const addClick$ = DOM
.select('.add')
.events('click')
.merge(firstNumber$)
.merge(secondNumber$)
.filter(value => console.log(value))
.scan((nb1, nb2) => nb1 + nb2)
return {
DOM: addClick$.map(result =>
div([
input('.first-number',{type:'text'}),
input('.second-number',{type:'text'}),
button('.add', 'Add'),
h2('Result is : ' + result)
])
)
};
}
It doesn't work at all and I can't figure out in my mind what I m doing wrong out there.
I m looking for a simple explanation how can I make this working ? I feel just like the merging streams of secondNumber$ and firstNumber$ are not correct and I can't find why..
Any idea ?
EDIT : I got that I shouldn't use the operator I was using, but use withLatestFrom.
The fact is that I m using xstream and so I have to map / flatten :
import {
div,
h1,
input,
button
} from '#cycle/dom';
/**
* Counter
* #param {Object} sources Contains the inputs
* #return {Object} The output sinks
*/
function counter(sources) {
const input1$ = sources.DOM
.select('.input1')
.events('input')
.map(ev => ev.target.value)
.startWith(0);
const input2$ = sources.DOM
.select('.input2')
.events('input')
.map(ev => ev.target.value)
.startWith(0);
const add$ = sources.DOM
.select('.add')
.events('click');
const resultAddition$ = add$
.map(ev => input1$
.map(value => input2$
.map(value2 => Number(value) + Number(value2)))
.flatten())
.flatten()
.startWith(0);
return {
DOM: resultAddition$.map(item => {
console.log(item); // triggered each time an input is modified
return div([
h1(`Super new value : ${item}`),
input('.input1', {
attrs: {
type: 'text'
}
}),
input('.input2', {
attrs: {
type: 'text'
}
}),
button('.add', 'Ajouter')
]);
})
};
}
export default counter;
From now, I have got in mind what the code should do, mapping on each click the operation and flatten the two input$ to get my result only when clicking the button
The fact is that the result value is changing on input and not and click. And more important, it changes on input only after the first click on the add button that is not what I want to.
What am I doing wrong this time ?
Thanks for your replies
It seems like you want combineLatest, not merge.
Both combineLatest and merge are "combination operators". They bring multiple Observables together and output one Observable. However, combineLatest is for "AND" combinations, while merge is for "OR" combinations.
You probably need "AND", because you want the value from first-number AND the value from second-number. That said, you want those values only when an add click happens. In that case, there is a variant of combineLatest called withLatestFrom. It allows you to sample the values from first-number AND second-number, but only when the add click happens.
const addClick$ = DOM
.select('.add')
.events('click')
const added$ = addClick$
.withLatestFrom(firstNumber$, secondNumber$,
(click, first, second) => first + second
)
As a side note, you should never do something like .filter(value => console.log(value)). The function for filter is a predicate. It's supposed to be a "condition" function that returns a boolean. If you want to debug, use .do(value => console.log(value)).
PS: I'm assuming you were using RxJS v4.

Categories

Resources