RxJS Observable: repeat using count and then using notifier - rxjs

I have an Observable that emits Either = Success | Failure:
import { Observable } from 'rxjs';
type Success = { type: 'success' };
type Failure = { type: 'failure' };
type Either = Success | Failure;
const either$ = new Observable<Either>(observer => {
console.log('subscribe');
observer.next({ type: 'failure' });
observer.complete();
return () => {
console.log('unsubscribe');
};
});
I want to allow the user to "retry" the observable when the Observable completes and the last value was Failure.
(The retry{,When} operators do not help here because they work with errors on the error channel. For this reason, I believe we should think in terms of repeat instead.)
I want to:
Repeat the Observable n times until the last value is not Failure.
Then, allow the user to repeat manually. When a repeat notifier observable (repeat$) emits, repeat the observable again.
For example:
// subscribe
// next { type: 'failure' }
// unsubscribe
// retry 2 times:
// subscribe
// next { type: 'failure' }
// unsubscribe
// subscribe
// next { type: 'failure' }
// unsubscribe
// now, wait for repeat notifications…
// on retry notification:
// subscribe
// next { type: 'failure' }
// unsubscribe

I couldn't come up with something simpler, but the code does what you want.
See https://stackblitz.com/edit/typescript-yqcejk
defer(() => {
let retries = 0;
const source = new BehaviorSubject(null);
return merge(source, repeat$.pipe(filter(() => retries <= MAX_RETRIES)))
.pipe(
concatMapTo(either$),
tap(value => {
const action = value as Either;
if (action.type === 'failure') {
if (retries < MAX_RETRIES) {
retries += 1;
source.next(null);
}
} else {
retries = 0;
}
})
)
}).subscribe(console.log);
I had to manually count retries.
The code has two sources of events source for automatic retries and repeat$ for user retries. All events are mapped to either$ using concatMapTo. As a side-effect we either next() to retry or do nothing waiting for user to retry.
User retries are suppressed using filter(() => retries >= MAX_RETRIES) until MAX_RETRIES count is reached.

Related

Re-execute async RxJS stream after delay

I'm using RxJS 6 to lazily step through iterable objects using code similar to example running below. This is working well but I'm having trouble solving my final use case.
Full code here
import { EMPTY, defer, from, of } from "rxjs";
import { delay, expand, mergeMap, repeat } from "rxjs/operators";
function stepIterator (iterator) {
return defer(() => of(iterator.next())).pipe(
mergeMap(result => result.done ? EMPTY : of(result.value))
);
}
function iterateValues ({ params }) {
const { values, delay: delayMilliseconds } = params;
const isIterable = typeof values[Symbol.iterator] === "function";
// Iterable values which are emitted over time are handled manually. Otherwise
// the values are provided to Rx for resolution.
if (isIterable && delayMilliseconds > 0) {
const iterator = values[Symbol.iterator]();
// The first value is emitted immediately, the rest are emitted after time.
return stepIterator(iterator).pipe(
expand(v => stepIterator(iterator).pipe(delay(delayMilliseconds)))
);
} else {
return from(values);
}
}
const options = {
params: {
// Any iterable object is walked manually. Otherwise delegate to `from()`.
values: ["Mary", "had", "a", "little", "lamb"],
// Delay _between_ values.
delay: 350,
// Delay before the stream restarts _after the last value_.
runAgainAfter: 1000,
}
};
iterateValues(options)
// Is not repeating?!
.pipe(repeat(3))
.subscribe(
v => {
console.log(v, Date.now());
},
console.error,
() => {
console.log('Complete');
}
);
I'd like to add in another option which will re-execute the stream, an indefinite number of times, after a delay (runAgainAfter). I'm having trouble composing this in cleanly without factoring the result.done case deeper. So far I've been unable to compose the run-again behavior around iterateValues.
What's the best approach to accomplish the use case?
Thanks!
Edit 1: repeat just hit me in the face. Perhaps it means to be friendly.
Edit 2: No, repeat isn't repeating but the observable is completing. Thanks for any help. I'm confused.
For posterity here is the full code sample for a revised edition is repeat-able and uses a consistent delay between items.
import { concat, EMPTY, defer, from, interval, of, throwError } from "rxjs";
import { delay, expand, mergeMap, repeat } from "rxjs/operators";
function stepIterator(iterator) {
return defer(() => of(iterator.next())).pipe(
mergeMap(result => (result.done ? EMPTY : of(result.value)))
);
}
function iterateValues({ params }) {
const { values, delay: delayMilliseconds, times = 1 } = params;
const isIterable =
values != null && typeof values[Symbol.iterator] === "function";
if (!isIterable) {
return throwError(new Error(`\`${values}\` is not iterable`));
}
// Iterable values which are emitted over time are handled manually. Otherwise
// the values are provided to Rx for resolution.
const observable =
delayMilliseconds > 0
? defer(() => of(values[Symbol.iterator]())).pipe(
mergeMap(iterator =>
stepIterator(iterator).pipe(
expand(v => stepIterator(iterator).pipe(delay(delayMilliseconds)))
)
)
)
: from(values);
return observable.pipe(repeat(times));
}
I'm gonna be honest, but there could be better solution for sure. In my solution, I ended up encapsulating delay logic in a custom runAgainAfter operator. Making it an independent part, that doesn't affect your code logic directly.
Full working code is here
And the code of runAgainAfter if anybody needs it:
import { Observable } from "rxjs";
export const runAgainAfter = delay => observable => {
return new Observable(observer => {
let timeout;
let subscription;
const subscribe = () => {
return observable.subscribe({
next(value) {
observer.next(value);
},
error(err) {
observer.error(err);
},
complete() {
timeout = setTimeout(() => {
subscription = subscribe();
}, delay);
}
});
};
subscription = subscribe();
return () => {
subscription.unsubscribe();
clearTimeout(timeout);
};
});
};
Hope it helps <3

Invoking observables with Subject next() not working

Why does this function only work once? I click a button to call the next() on the Subject queue which works but if I click the other button it doesn't work.
getData(text): Observable<string> {
const timer$ = timer(2000);
const observable = new Observable<string>(observer => {
timer$.pipe(
map(() => {
observer.next('http response ' + text);
})
).subscribe();
});
return observable;
}
I setup a Subject and use next() which should make the observable emit data.
queue = new Subject();
streamA$: Observable<string>;
streamB$: Observable<string>;
images$: Observable<string>;
constructor(private timerService: TimerService) {
}
ngOnInit() {
this.streamA$ = this.timerService.getData('a');
this.streamB$ = this.timerService.getData('b');
this.images$ = this.queue.pipe(concatMap((data: string) => data));
}
clickA() {
this.queue.next(this.streamA$);
}
clickB() {
this.queue.next(this.streamB$);
}
Template:
<button (click)="clickA()">Click A</button>
<button (click)="clickB()">Click B</button>
<div>{{images$ | async}}</div>
https://stackblitz.com/edit/angular-subject-queue
You're using concatMap(). This emits all the events emitted from the first observable emitted by the subject, then all the events emitted by the second observable emitted by the subject.
But the first observable never completes, so there's no way for the second observable to ever emit anything.
If you want the observable returned by the service to emit once after 2 seconds then complete, all you need is
return timer(2000).pipe(
map(() => 'http response ' + text)
);

Dispatching action when a custom observable is cancelled

The newest version of redux-observable doesn't give any access to store and we have to dispatch actions by returning them from epics. I don't know how to dispatch actions when some observables are cancelled, for example by switchMap effect. For instance:
const counter = new Observable(observer => {
let i = 0;
const timer = setInterval(() => {
observer.next(i++);
}, 1000);
return {
unsubscribe: () => {
clearInterval(timer);
// how to dispatch action here like { type: 'COUNTER_ABORTED' }?
}
};
});
Or maybe this is RXJS antipattern what I want to do? Maybe there is some switchMap trick I could use to do additional things for cancelled observables?

What the unexpected behavior Observable RxJS with async functions and toPromise?

When i use only subscribe-method, it works truthy, but with this code - i don't understand the result.
const Observable = require("rxjs").Observable;
let i = 0;
const x = new Observable((o) => {
setInterval(() => o.next(++i), 1000);
});
(async () => {
while (true) {
try {
console.log("loop");
console.log("value", await x.toPromise());
} catch (e) {
console.log(e);
}
}
})();
x.subscribe((value) => {
console.log("subscribe", value);
});
This code result is:
loop
subscribe 2
subscribe 4
subscribe 6
subscribe 8
subscribe 10
subscribe 12
subscribe 14
What's happened?
It works same with this variant of using toPromise
function a() {
x.toPromise().then((value) => {
console.log("promise", value);
a();
}).catch((e) => {
console.log("error", value);
});
}
a();
toPromise() is executed on an Observable on its completion. Since your observable is never actually completed, it does not execute. Use take(1) to force it to emit value before the completion of the observable.
const Observable = require("rxjs").Observable;
let i = 0;
const x = new Observable((o) => {
setInterval(() => o.next(++i), 1000);
});
(async () => {
while (true) {
try {
console.log("loop");
console.log("value", await x.take(1).toPromise());//here
} catch (e) {
console.log(e);
}
}
})();
x.subscribe((value) => {
console.log("subscribe", value);
});
Output:
loop
subscribe 2
value 1
loop
subscribe 4
value 5
loop
subscribe 7
value 9
loop
subscribe 11
value 14
As for the values:
take() will complete once atleast one value is emitted regardless of whether the source observable completes. So it really depends on what value the observable is emitting the next time the toPromise() is called

Chain Observable Queue

Coming from the Promise world, I can implement a queue function that returns a Promise that won't execute until the previous Promise resolves.
var promise = Promise.resolve();
var i = 0;
function promiseQueue() {
return promise = promise.then(() => {
return Promise.resolve(++i);
});
}
promiseQueue().then(result => {
console.log(result); // 1
});
promiseQueue().then(result => {
console.log(result); // 2
});
promiseQueue().then(result => {
console.log(result); // 3
});
// -> 1, 2, 3
I'm trying to recreate this queue-like function using Observables.
var obs = Rx.Observable.of(undefined);
var j = 0;
function obsQueue() {
return obs = obs.flatMap(() => {
return Rx.Observable.of(++j);
});
}
obsQueue().subscribe(result => {
console.log(result); // 1
});
obsQueue().subscribe(result => {
console.log(result); // 3
});
obsQueue().subscribe(result => {
console.log(result); // 6
});
// -> 1, 3, 6
Every time I subscribe, it re-executes the history of the Observable, since at the time of subscription the "current Observable" is actually an Observable which emits multiple values, rather than the Promise that just waits until the last execution has completed.
flatMap isn't the answer for this use case, and nearly all the "chain" and "queue" answers I can find online are about chaining several Observables that are part of one overall Observable, where flatMap is the correct answer.
How can I go about creating the above Promise queue function using Observables?
For context, this queue function is being used in a dialog service, which dictates only one dialog can be shown at a time. If multiple calls are made to show different dialogs, they only appear one at a time in the order that they were called.
If you change:
return obs = obs.flatMap...
With
return obs.flatMap...
You will see the same output as you do with promises (1, 2, 3).
To chain observables such that the next one is not executed until the previous one is complete, use the concat operator
let letters$ = Rx.Observable.from(['a','b','c']);
let numbers$ = Rx.Observable.from([1,2,3]);
let romans$ = Rx.Observable.from(['I','II','III']);
letters$.concat(numbers$).concat(romans$).subscribe(e=>console.log(e));
//or...
Rx.Observable.concat(letters$,numbers$,romans$).subscribe(e=>console.log(e));
// results...
a b c 1 2 3 I II III
Live demo
Figured it out! May not be quite as elegant as the Promise chain, and I'm definitely open to suggestions to clean it up.
var trigger = undefined;
function obsQueue() {
if (!trigger || trigger.isStopped) {
trigger = new Rx.Subject();
return createObservable(trigger);
} else {
var lastTrigger = trigger;
var newTrigger = trigger = new Rx.Subject();
return lastTrigger.last().mergeMap(() => {
return createObservable(newTrigger);
});
}
}
var j = 0;
function createObservable(trigger) {
// In my use case, this creates and shows a dialog and returns an
// observable that emits and completes when an option is selected.
// We want to make sure we only create the next dialog when the previous
// one is closed.
console.log('creating');
return new Rx.Observable.of(++j).finally(() => {
trigger.next();
trigger.complete();
});
}
obsQueue().subscribe(result => {
console.log('first', result);
});
obsQueue().subscribe(result => {
console.log('second', result);
});
obsQueue().subscribe(result => {
console.log('third', result);
});
var timer = setTimeout(() => {
obsQueue().subscribe(result => {
console.log('fourth', result);
});
}, 1000);
// Output:
// creating
// first 1
// creating
// second 2
// creating
// third 3
// creating
// fourth 4
Rather than try to figure out how to chain them in order, I have each observable create its own trigger to let the next observable know when to create itself.
If all the triggers have been completed (setTimeout case, we queue up another one later), then the queue starts again.

Resources