How to replace the deprecated repeatWhen(notifier) with repeat(delay) - rxjs

While testing, refactoring and future-proofing a customers project, I stumbled over this little deprecation notification:
Will be removed in v9 or v10. Use repeat's delay option instead.
repeatWhen(notifier: (notifications: Observable) =>
Observable): MonoTypeOperatorFunction
Simple enough, right? But when I tried, I didn't find a simple way to do so. I have a rough idea how I could hack it. But that's not exactly what I'd like to hand over to a customer as "improved" code. So what obvious path do I fail to see, that leads from this (straight out of the rxjs documentation):
import { of, fromEvent, repeatWhen } from 'rxjs';
const source = of('Repeat message');
const documentClick$ = fromEvent(document, 'click');
***const result = source.pipe(repeatWhen(() => documentClick$));***
result.subscribe(data => console.log(data))
to this:
import { of, fromEvent, repeat } from 'rxjs';
const source = of('Repeat message');
const documentClick$ = fromEvent(document, 'click');
const result = source.pipe(repeat({ delay: ??? () => documentClick$) });
result.subscribe(data => console.log(data))
How to switch an option that accepts a number into an option that repeats whenever the event happens? Well, as said, I have an idea how to achieve it, but it would be incredibly ugly. So what am I missing?

You almost got it right, just remove the question marks :)
source.pipe(repeat({ delay: () => documentClick$ }));
This basically says "Whenever source completes, subscribe to documentClick$ and whenever this (documentClick$) emits, re-subscribe to source.
Did you want to do more with that? I didn't fully understand your last paragraph.
In my little example, where a mouse click is faked by a timer, I get an emission every 2 seconds:
import { of, repeat, timer } from 'rxjs';
const source = of('Repeat message');
const documentClick$ = timer(2000);
const result = source.pipe(repeat({ delay: () => documentClick$ }));
result.subscribe((data) => console.log(data));

Related

Suggestions to fix a flaky test in cypress

I have a map-based application (like Google Maps) and I am writing tests for the zoom-in option. The test covering all the zoom levels when zooming in. My test is working but the result is not consistent and it is flaky.
My code:
static verifyAllAvailableZoomInZoomSizeInNM() {
var expectedValues = [500,200,100,50,20,10,5,2,1,0.5,0.2,0.1,0.05,0.02,0.01,0.005,0.002,0.001,0.0005,0.0002,0.0001,0.00005,0.00002];
cy.getCurrentZoomSizes({
numValues: 26,//I expect to give 23 but I just gave 26 in order to capture all sizes
waitBetween: 1000,
elementLocator: BTN_MAP_ZOOMIN,
}).should("be.eql", expectedValues);
}
Cypress Commands:
/* Get the numeric value of zoom size in nm */
Cypress.Commands.add("getCurrentZoomSize", () => {
cy.get('div[class="ol-scale-line-inner"]').then(
($el) => +$el[0].innerText.replace(" nm", "")
);
});
/* Get a sequence of zoom size values */
Cypress.Commands.add("getCurrentZoomSizes", ({ numValues, waitBetween, elementLocator }) => {
const values = [];
Cypress._.times(numValues, () => {
cy.getCurrentZoomSize()
.then((value) => values.push(value))
cy.get(elementLocator)
.click()
.wait(waitBetween);
});
return cy.wrap(values);
});
And the test result1:
test result2:
As you can see in the screenshots, a few of the zoom sizes had duplicated. I tried giving enough wait between each zoom-in click but it is not helping either. Is there any way, I can fix this flaky test?
The loop executes a lot faster than the Cypress commands or the zoom operation, you can see it if you add a console.log() just inside the loop
Cypress._.times(numValues, (index) => {
console.log(index)
That's not necessarily a problem, it just fills up the command queue really quickly and the commands then chug away.
But in between getCurrentZoomSize() calls you need to slow things down so that the zoom completes, and using .wait(waitBetween) is probably why thing get flaky.
If you apply the .should() to each zoom level, you'll get retry and wait in between each zoom action.
The problem is figuring out how to arrange things so that the proper retry occurs.
If you do
cy.getCurrentZoomSize()
.should('eq', currentZoom);
which is equivalent to
cy.get('div[class="ol-scale-line-inner"]')
.then($el => +$el[0].innerText.replace(" nm", "") )
.should('eq', currentZoom);
it doesn't work, the conversion inside the .then() gets in the way of the retry.
This works,
cy.get('div[class="ol-scale-line-inner"]')
.should($el => {
const value = +$el[0].innerText.replace(" nm", "")
expect(value).to.eq(expectedValue)
})
or this
cy.get('div[class="ol-scale-line-inner"]')
.invoke('text')
.should('eq', `${currentZoom} nm`);
So the full test might be
Cypress.Commands.add("getCurrentZoomSizes", (expectedValues, elementLocator) => {
const numValues = expectedValues.length;
Cypress._.times(numValues, (index) => {
const currentZoom = expectedValues[index];
cy.get('div[class="ol-scale-line-inner"]')
.invoke('text')
.should('eq', ${currentZoom} nm`); // repeat scale read until zoom finishes
// or fail if never gets there
cy.get(elementLocator).click(); // go to next level
});
});
const expectedValues = [...
cy.getCurrentZoomSizes(expectedValues, BTN_MAP_ZOOMIN)

rxjs: cancelling a debounced observable

I have an observable Subject that emits some changes with debouncing:
someSubject.pipe(
debounceTime(5000),
).subscribe(response => {
console.log('Value is', response);
})
Now, I need a Stop button somewhere on the screen that would cancel my debounced emit. So I create a button:
const stopObs = new Subject();
...
<button onClick={() => stopObs.next()}>Stop</button>
and modify my subscription like so:
someSubject.pipe(
debounceTime(5000),
takeUntil(stopObs),
).subscribe(response => {
console.log('Value is', response);
})
This works fine, after hitting "Stop" I stop getting values in console, but there is a problem: the observable is stopped forever. And I need it to be able to emit new values, I only need to cancel already started debounced emits.
My first thought was to create a new subject and use repeatWhen:
const startObs = new Subject();
...
<button onClick={() => startObs.next()}>Start</button>
...
someSubject.pipe(
debounceTime(5000),
takeUntil(stopObs),
repeatWhen(() => startObs)
).subscribe(response => {
console.log('Value is', response);
})
But there's another problem: if I hit "Start" button more than one time and emit more than one value to startObs, then I start getting multiple console.log's for single debounced value!
So is there a way to cancel only debounced emits without stopping the entire observable?
Since debounceTime is just
const duration = timer(dueTime, scheduler);
return debounce(() => duration);
I think you can solve the problem like this:
someSubject.pipe(
debounce(() => timer(5000).pipe(takeUntil(stopObs))),
)
If you want to send the last value when the timer is cancelled due to stopObs, you could try this:
someSubject.pipe(
debounce(
() => timer(5000)
.pipe(
takeUntil(stopObs),
isEmpty(),
)
),
)
isEmpty() will emit true immediately before a complete notification, which is what debounce needs in order to send the last received value. If the timer completes without stopObs's involvement, isEmpty will emit false instead of true, but this still works well for debounce, since it only needs a value from the inner observable.

Handle mousemove stop with RxJs

Is there a way to identify when user stopped moving the mouse? I can't figure it out how to tell in RxJs if user stopped for let's say 2s.
When I use it like this:
fromEvent(document, 'mousemove').pipe(
debounceTime(2000)
).subscribe(() => console.log("Stoped"));
It triggers even if I move cursor out of the screen.
Depends on a situation, but timeout or timeoutWith may work for you.
This example will emit "stop" every time when there was no mousemove events for more than 1s:
const { of, defer, concat, fromEvent } = rxjs;
const { mapTo, timeoutWith, skipUntil } = rxjs.operators;
const move$ = fromEvent(document, 'mousemove').pipe(mapTo('move'));
const moveAndStop$ = move$.pipe(
timeoutWith(
1000,
defer(() => concat(
of('stop'),
moveAndStop$.pipe(skipUntil(move$))
))
)
);
moveAndStop$
.subscribe(e => console.log(e));
<script src="https://unpkg.com/rxjs#6.3.2/bundles/rxjs.umd.min.js"></script>
<div id="app">Move your mouse over me</div>

Draggable component implementation with rxjs

I moving implementation of draggable component from rxjs5 to 6. While testing i noticed that function onDragEnd is not called occasionally. What could be the reason for this ? Here is the code:
fromEvent(this.ref,"mousedown")
.pipe(
filter(this.inside),
debounceTime(300),
tap((event) => {
this.onDragStart(event);
}),
exhaustMap(() =>
fromEvent(document.body,"mousemove")
.pipe(
takeUntil(
fromEvent(document.body,"mouseup")
.pipe(tap(this.onDragEnd)) // <--- my problem
)
)
)
)
.subscribe(
(event) => {
this.onDragMove(event);
},
this.onError
)
Edit: just to clarify what i expect, mousedown event should be map to observable which (assuming i understood exhaustMap correctly) should not allow to outer observable emit again until inner observable completes. What i cant understand is why in one of 20-30 times onDragEnd is not fired, observable from document so as my intuition telling me mouse-up is inevitable in this case, am i wrong ? If you have some ideas what could possibly be wrong with this implementation please let me know, thank you!

How to code a drag-lock using RxJS?

A drag-lock interaction starts with a first double-click, followed by some mouse moves, and ends with a second double-click. It is a variant of the drag-and-drop.
I want to code this interaction in RxJS, but the big problem I face is that the same event (double-click) starts and ends the interaction (a problem we do not have while coding a drag-and-drop).
How to implement a drag-lock using RxJS?
I want to subscribe to the first-click, to all the moves, and the final double-click.
plunker: https://plnkr.co/edit/MXUtPzH9iD40VjVxLBwB?p=preview
Lets define an event - DragLockEvent which consists of type "DragLockEventType" which could be either 'initialClick', 'move' or 'finalClick', and the mouse move event, in case it is of type 'move'.
Now let's create an observable which will emit these events: dragLock$.
Inside the observable there's a flag - "dragging", indicating whether or not we are in the middle of dragging.
Now we need to listen to double click events, and for each such event we need to toggle the dragging flag, and to emit the matching event.
Then, in case we are in dragging mode, we start listening to mouse move events and report them, until we have another double click.
type DragLockEventType = 'initialClick' | 'move' | 'finalClick';
interface DragLockEvent {
type: DragLockEventType;
moveEvent?: MouseEvent;
}
const dragLock$: Observable<DragLockEvent> = Observable.create((observer: Observer<DragLockEvent>) => {
let dragging = false;
const doubleClick$ = fromEvent(document.documentElement, 'dblclick')
.pipe(takeWhile(() => !observer.closed));
const mouseMove$ = fromEvent(document.documentElement, 'mousemove')
.pipe(takeWhile(() => !observer.closed));
doubleClick$
.pipe(
tap(() => {
dragging = !dragging;
if (dragging) observer.next({ type: 'initialClick' });
else observer.next({ type: 'finalClick' });
}),
filter(() => dragging),
switchMap(() => mouseMove$.pipe(takeUntil(doubleClick$)))
)
.subscribe((e: MouseEvent) => observer.next({ type: 'move', moveEvent: e }))
});

Resources