rxjs fundamentals | expecting subscription next to run once but runs multiple times - rxjs

fromEvent(document.querySelector('input[name=secondaryPhone]'), 'blur').pipe(
first(),
map(event => event.target['value']),
).subscribe(value => {
debugger
})
I've been working with and learning rxjs for over a month now and still have long way to go. Would anyone be able to explain to me why my debugger is triggered an equal amount of times as there was characters entered into the input? e.g., 123 4567 triggers the debugger 7 times (all with value = 123 4567). Thanks!

It only runs once for me. This is not the event handler that is triggering the multiple debuggers for you.
const { fromEvent } = rxjs;
const { first, map } = rxjs.operators;
fromEvent(document.querySelector('input[name=secondaryPhone]'), 'blur').pipe(
first(),
map(event => event.target['value']),
).subscribe(value => {
console.log('blur ' + value);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.6.0/rxjs.umd.min.js"></script>
<input name="secondaryPhone">

Related

Multiple subscriptions causing source stream to fire twice

I want to use RxJS to listen to clicks, perform a transaction, and keep track of the transaction status:
function performTransaction() {
const status = {
'0': 'pending',
'1': 'success'
}
return interval(1000).pipe(take(2), map((i) => status[`${i}`]))
}
const click$ = new Subject<void>()
const claimTxnState$ = click$.pipe(switchMap(() => {
console.log('performing transaction') // -> runs once which is expected
return performTransaction()
}))
claimTxnState$.subscribe((v) => {
console.log('claimTxnState', v);
})
export default function App() {
return (
<div className="App">
<button onClick={() => click$.next()}>click me</button>
</div>
);
}
This causes performing transaction to be output once, as is expected.
But I want to pipe claimTxnState$ to more places:
const claimIsPending$ = claimTxnState$.pipe(map((claim) => claim === 'pending'))
claimIsPending$.subscribe((v) => {
console.log('claimIsPending', v);
})
This now causes performing transaction to be output twice, which I understand because these are cold observables that get recreated on each subscription. But I don't want this. I only want my performTransaction to get called once. How can I achieve this?
Complete example.
I found the answer as I was typing the question. I need to use the share operator to convert the observable from cold to hot. This has the effect of sharing claimTxnState$ between all subscribers (i.e. .pipes(...)):
const claimTxnState$ = click$.pipe(
switchMap(() => {
console.log('performing transaction')
return performTransaction()
}),
share() // <- this is the interesting line
)
Sandbox.
More detail.

How to pause a buffer from one source in RXJS?

I have a stream of events coming through via fromEventPattern like so:
fromEventPattern<IPsEvent>(addEventHandler).subscribe(ps$);
Due to business quirks, I expect that I will sometimes get an exception thrown, at which point I want to queue up the events and refire once that error state is resolved.
I've been trying the solution from Pausable buffer with RxJS to no avail. I am thinking it's because they are able to toggle through a separate observable whereas this is kind of asking to pause itself midstream. In the linked example I have blockingCallsAllowed$ rather than autoSave$. Here is my latest try:
const source$ = new Subject<IPsEvent>();
const blockingCallsAllowed$ = new BehaviorSubject(true);
const on$ = blockingCallsAllowed$.pipe(filter((v) => v));
const off$ = blockingCallsAllowed$.pipe(filter((v) => !v));
source$
.pipe(
map(() => {
try {
// line will throw exception at certain times
myFunction();
return true;
} catch (e) {
const i = setInterval(() => {
try {
myFunction();
console.log('good again');
blockingCallsAllowed$.next(true);
clearInterval(i);
} catch (er) {
// still in flux
}
}, 50);
return false;
}
}),
)
.subscribe(blockingCallsAllowed$);
const output$ = merge(
source$.pipe(bufferToggle(off$, () => on$)),
source$.pipe(windowToggle(on$, () => off$)),
).pipe(concatMap(from));
output$.subscribe((evt) => {
console.log('After buffers', evt);
});
// Add events from the Ps API to the event stream
fromEventPattern(addEventHandler).subscribe(source$);
Everything fires fine until the first exception and then it never outputs what it had buffered away, even though it fires that things are good again in console.log.
I am thinking there is some timing issue around relying on source$.pipe in the same execution and then the interval running later with .next. Can't nail it though after many different permutations of this code.
It's not clear to me what you're trying to implement. Though if you want to keep retrying myFunction() every 50ms until it succeeds and stop processing other events while this happens, concatMap basically does all that for you.
It will buffer emissions from the source while it waits for the inner observable to complete.
So what you're after might look like this:
source$.pipe(
concatMap(_ => of(true).pipe(
tap(_ => myFunction()),
retryWhen(errors => errors.pipe(
delay(50)
))
))
).subscribe();

Repeat or restart an observable (interval) after its completed

Here is what I am trying to do - step #3 is what I am struggling with.
Set custom interval
trigger an http call (subscribe to it)
Once interval is finished, I want to repeat the above steps indefinitely (until a condition is met)
Steps #1 & #2 are working
setRenewalInterval() {
...
return this.timerSub = interval(CUSTOM_INTERVAL * 1000)
.pipe(
take(1),
map(x => {
if (this.isLoggedIn && !tokenHelper.isTokenExpired()) {
console.log("<Requesting Token Renew>")
this.getNewToken().subscribe(
x => {
console.log("<Token renewed successfully>");
},
err => {
console.log("<Token failed to renew>");
this.setLogOut();
},
() => console.log('<renewal observable completed>')
);
}
else {
console.log("<Not Logged in or Token expired; logging out>");
this.setLogOut();
}
})
)
.subscribe();
}
#3 repeat the above steps after the subscription is completed. Either call setRenewalInterval() or preferably do it inside the setRenewalInterval().
I have looked into rxjs repeat() and expand() but I can't get them to work with the above code. Any help is greatly appreciated, especially a testable/runnable code e.g. jsfiddle.
You're really close, interval(...) is already creating an emission indefinitely however take(1) is causing the entire observable to complete after the first emission.
If you remove the take(1) your code inside the map will be called at your interval forever.
map is a useful function when you want to take values emit in an observable and convert it to a new value synchronously, similar to how Array.map() works. Because you're working with observables inside this map we can use something like mergeMap() which will handle the subscription and piping through of values automatically.
setRenewalInterval() {
// ...
return this.timerSub = interval(CUSTOM_INTERVAL * 1000)
.pipe(
mergeMap(() => {
if (this.isLoggedIn && !tokenHelper.isTokenExpired()) {
return this.getNewToken();
} else {
return this.setLogOut();
}
})
)
.subscribe(x => {
// x is the result of every call to getNewToken() or setLogOut()
});
}

RxJs: Pattern for observable search results

My scenario is a classic web page with a search form and a result list. I want to encapsulate the behavior of loading the results in an Observable.
Here's what I'm currently doing in TypeScript:
function loadResults(query): Observable<T[]> {}
const results = new Subject<ResultEvent<T[]>>();
const results: Observable<ResultEvent<T[]>> =
form.valueChanges
.distinctUntilChanged()
.do(() => results.next(ResultEvent.pending()))
.switchMap(query => loadResults(query))
.subscribe({
next: (data: T[]) => results.next(ResultEvent.present(data)),
error: err => results.next(ResultEvent.failed(err)),
});
The idea is that results always contains the current state of the search: either pending, present or failed. When the query changes, the result is set to pending, and when the service returns data, the result is set to present.
What I don't like about this solution is the explicit call to subscribe(). I'd rather have a simple Observable that can be subscribed an unsubscribed from (eg. in Angular with the async pipe), without creating an explicit subscribtion. The side-effects in do also seem rather hacky.
const results: Obserbable<ResultEvent<T[]>> =
form.valueChanges.distinctUntilChanged()
. /* here be dragons */;
Thanks for any advice and ideas!
I think you want something along these lines:
const results$ = form.valueChanges
// This is up to you, but with user input it might make sense to
// give it just a little bit of time before we hit the server since
// most user input will be more than a single character.
//.debounceTime(100)
.distinctUntilChanged()
// Using switchMap guarantees that the inner observable will be
// cancelled if the input changed while we are still waiting for
// a result. Newer is always better!
.switchMap(query => loadResults(query)
// If we get data, we use it.
.map(results => ResultEvent.present(results))
// We catch errors and turn them into a failure event.
.catch(err => Observable.of(ResultEvent.failed(err)))
// Whatever happens, first things first.
.startWith(ResultEvent.pending())
);
I would also think about adding a debounceTime in there, by the way.
Here's a snippet you can copy-paste into https://rxviz.com to see it in action (unfortunately their share link feature doesn't work anymore). Make sure to set the time window to something like 10 seconds.
const ResultEvent = {
pending: () => 'Pending',
failed: err => 'Error: ' + err,
present: data => 'Data: ' + data,
};
const loadResults = query => query === 2
? Rx.Observable.of(null).delay(500).switchMap(() => Rx.Observable.throw('Oops'))
: Rx.Observable.of(42).delay(500)
const input$ = Rx.Observable.timer(0, 2000).take(4);
input$.switchMap(query => loadResults(query)
.map(data => ResultEvent.present(data))
.catch(err => Rx.Observable.of(ResultEvent.failed(err)))
.startWith(ResultEvent.pending())
)

How to test an inner Observable that will not complete?

I'm using jest to test a redux-observable epic that forks off an inner observable created using Observable.fromEvent and listens for a specific keypress before emitting an action.
I'm struggling to test for when the inner Observable does not receive this specific keypress and therefore does not emit an action.
Using jest, the following times out:
import { Observable, Subject } from 'rxjs'
import { ActionsObservable } from 'redux-observable'
import keycode from 'keycode'
const closeOnEscKeyEpic = action$ =>
action$.ofType('LISTEN_FOR_ESC').switchMapTo(
Observable.fromEvent(document, 'keyup')
.first(event => keycode(event) === 'esc')
.mapTo({ type: 'ESC_PRESSED' })
)
const testEpic = ({ setup, test, expect }) =>
new Promise(resolve => {
const input$ = new Subject()
setup(new ActionsObservable(input$))
.toArray()
.subscribe(resolve)
test(input$)
}).then(expect)
// This times out
it('no action emitted if esc key is not pressed', () => {
expect.assertions(1)
return testEpic({
setup: input$ => closeOnEscKeyEpic(input$),
test: input$ => {
// start listening
input$.next({ type: 'LISTEN_FOR_ESC' })
// press the wrong keys
const event = new KeyboardEvent('keyup', {
keyCode: keycode('p'),
})
const event2 = new KeyboardEvent('keyup', {
keyCode: keycode('1'),
})
global.document.dispatchEvent(event)
global.document.dispatchEvent(event2)
// end test
input$.complete()
},
expect: actions => {
expect(actions).toEqual([])
},
})
})
My expectation was that calling input$.complete() would cause the promise in testEpic to resolve, but for this test it does not.
I feel like I'm missing something. Does anyone understand why this is not working?
I'm still new to Rx/RxJS, so my apologies if the terminology of this answer is off. I was able to reproduce your scenario, though.
The inner observable (Observable.fromEvent) is blocking the outer observable. The completed event on your ActionsObservable doesn't propagate through until after the inner observable is completed.
Try out the following code snippet with this test script:
Run the code snippet.
Press a non-Escape key.
Nothing should be printed to the console.
Select the "Listen for Escape!" button.
Press a non-Escape key.
The keyCode should be printed to the console.
Select the "Complete!" button.
Press a non-Escape key.
The keyCode should be printed to the console.
Press the Escape key.
The keyCode should be printed to the console
The onNext callback should print the ESC_PRESSED action to the console.
The onComplete callback should print to the console.
document.getElementById('complete').onclick = onComplete
document.getElementById('listenForEsc').onclick = onListenForEsc
const actions = new Rx.Subject()
const epic = action$ =>
action$.pipe(
Rx.operators.filter(action => action.type === 'LISTEN_FOR_ESC'),
Rx.operators.switchMapTo(
Rx.Observable.fromEvent(document, 'keyup').pipe(
Rx.operators.tap(event => { console.log('keyup: %s', event.keyCode) }),
Rx.operators.first(event => event.keyCode === 27), // escape
Rx.operators.mapTo({ type: 'ESC_PRESSED' }),
)
)
)
epic(actions.asObservable()).subscribe(
action => { console.log('next: %O', action) },
error => { console.log('error: %O', error) },
() => { console.log('complete') },
)
function onListenForEsc() {
actions.next({ type: 'LISTEN_FOR_ESC' })
}
function onComplete() {
actions.complete()
}
<script src="https://unpkg.com/rxjs#5.5.0/bundles/Rx.min.js"></script>
<button id="complete">Complete!</button>
<button id="listenForEsc">Listen for Escape!</button>
Neither the switchMapTo marble diagram nor its textual documentation) clearly indicate what happens when the source observable completes before the inner observable. However, the above code snippet demonstrates exactly what you observed in the Jest test.
I believe this answers your "why" question, but I'm not sure I have a clear solution for you. One option could be to hook in a cancellation action and use takeUntil on the inner observable. But, that might feel awkward if that's only ever used in your Jest test.
I can see how this epic/pattern wouldn't be a problem in a real application as, commonly, epics are created and subscribed to once without ever being unsubscribed from. However, depending on the specific scenario (e.g. creating/destroying the store multiple times in a single application), I could see this leading to hung subscriptions and potential memory leaks. Good to keep in mind!

Resources