Sequence asynchronous redux-observable actions - rxjs

I'd like to sequence multiple async actions and when they are all finished, emit final actions:
const initializeEpic = (action$, state$, dependencies) =>
action$.ofType("INITIALIZE_APP_REQUEST").pipe(
mergeMap((action) =>
[
getUserRequest(),
getCoreDataRequest(),
// many other requests ...
// now here I want to subscribe to GET_USER_SUCCESS,
// GET_CORE_DATA_SUCCESS and other request "success" type's and
// once all are called, call initializeApp.success()
// keep in mind that "..._SUCCESS" actions are called from
// corresponding getUserEpic, getCoreDataEpic, etc...
]
)
)
I'm not sure if I'm trying to implement epic composition with good approach, but my idea in short is:
Call parent epic with PARENT_REQUEST action
Call child epics via corresponding *_CHILD_REQUEST action (child epics would catch it via action$.ofType(*_CHILD_REQUEST))
Subscribe to *_CHILD_SUCCESS action within parent epic (which is dispatched from child epic)
Once all *_CHILD_SUCCESS actions are called, call PARENT_SUCCESS

I believe you are looking for forkJoin. You can do something like this:
const loadEpic = (action$, state$) =>
action$.ofType("INITIALIZE_APP_REQUEST").pipe(
mergeMap((action) =>
forkJoin([
getUserRequest(),
getCoreDataRequest()
]).pipe(
map(([users, coreData]) =>
initializeApp.success(users, coreData)
)
)
)
)
Please note that the requests are done simultaneously and not sequentially. You don't have to make it in sequence as long as they don't depend on each other.
Also, getUserRequest() and getCoreDataRequest() must return an observable, for eg:
function getUserRequest() {
return from(axios.get(`/endpoint`)).pipe(map(response => response.data));
}

Related

Subject-like RxJS Observable that transparently pipes through flatMap

When using Dependency injection in Angular I often need to subscribe to an observable that I haven't yet created!
I often end up using something like this:
// create behavior subject OF Observable<number>
const subject = new BehaviorSubject<Observable<number>>(EMPTY);
// subscribe to it, using flatMap such as to 'unwrap' the observable stream
const unwrappedSubject = subject.pipe(flatMap((x: number) => x));
unwrappedSubject.subscribe(s => console.log(s));
// now actually create the observable stream
const tim = timer(1000, 1000);
// set it into the subject
subject.next(tim);
This uses flatMap to 'unwrap' the observable contained in the subject.
This works fine, but frankly it always feels 'icky'.
What I really want is something like this, where the consumer of the subject treats the instance of the Subject as Observable<number> without having to pipe it every usage.
const subject = new UnwrappingBehaviorSubject<number>(EMPTY);
subject.subscribe((x: number) => console.log(x));
// this could use 'next', but that doesn't feel quite right
subject.setSource(timer(1000, 1000));
I'm aware that I could subscribe to the timer and hook it up directly to the subject, but I also want to avoid an explicit subscribe call because that complicates the responsibility of unsubscribing.
timer(1000, 1000).subscribe(subject);
Is there a nice way to achieve this?
The Subject.ts and BehaviorSubject.ts source files get more complicated than I expected. I'm scared I'll end up with horrible memory leaks if I try to fork it.
I think this would be another way to solve it:
foo.component.ts
export class FooComponent {
private futureObservable$ = new Observable(subscriber => {
// 'Saving' the subscriber for when the observable is ready.
this.futureObservableSubscriber = subscriber;
// The returned function will be invoked when the below mentioned subject instance
// won't have any subscribers(after it had at least one).
return () => this.futureObservableSubscription.unsubscribe();
}).pipe(
// You can mimic the Subject behavior from your initial solution with the
// help of the `share` operator. What it essentially does it to *place*
// a Subject instance here and if multiple subscriptions occur, this Subject instance
// will keep track of all of them.
// Also, when the first subscriber is registered, the observable source(the Observable constructor's callback)
// will be invoked.
share()
);
private futureObservableSubscriber = null;
// We're using a subscription so that it's easier to collect subscriptions to this observable.
// It's also easier to unsubscribe from all of them at once.
private futureObservableSubscription = new Subscription();
constructor (/* ... */) {};
ngOnInit () {
// If you're using `share`, you're safe to have multiple subscribers.
// Otherwise, the Observable's callback(i.e `subscriber => {...}`) will be called multiple times.
futureObservable$.subscribe(/* ... */);
futureObservable$.subscribe(/* ... */);
}
whenObservableReady () {
const tim = timer(1000, 1000);
// Here we're adding the subscription so that is unsubscribed when the main observable
// is unsubscribed. This part can be found in the returned function from the Observable's callback.
this.futureObservableSubscription.add(tim.subscribe(this.futureObservableSubscriber));
}
};
Indeed, a possible downside is that you'll have to explicitly subscribe, e.g in the whenObservableReady method.
With this approach you can also have different sources:
whenAnotherObservableReady () {
// If you omit this, it should mean that you will have multiple sources at the same time.
this.cleanUpCrtSubscription();
const tim2 = timer(5000, 5000);
this.futureObservableSubscription.add(tim2.subscribe(this.futureObservableSubscriber));
}
private cleanUpCrtSubscription () {
// Removing the subscription created from the current observable(`tim`).
this.futureObservableSubscription.unsubscribe();
this.futureObservableSubscription = new Subscription();
}

Question about 2 rxjs observables and a timer

I have a question about rxjs in the context of nestjs CQRS sagas.
Suppose I have a scenario where there is two events being published one after another one. One sets the value and the other one unsets it. I need to be able to listen to the event which sets the value for 3 seconds and perform some action if another event has not been published in the meantime.
Here is some code for starters:
valuePersisted = (events$: Observable<any>): Observable<ICommand> => {
return events$
.pipe(
ofType(ValueHasBeenSet),
map(event => {
return new SomeCommand();
}),
);
}
I need to listen to ValueHasBeenUnset event somehow and cancel out of the stream in case this event was received within some time.
EDIT
I just realized that events ValueHasBeenSet and ValueHasBeenUnset can have different value types to be set and unset and code should distinguish that. For example both events have a property called type and its value can be 'blue' | 'yellow'. Is there a way to preserve the logic per event type keeping only two generic events ValueHasBeenSet and ValueHasBeenUnset?
Consider implementing it in the following way:
return events$
.pipe(
ofType(ValueHasBeenSet), // <- listen for ValueHasBeenSet event
switchMap(x => { // <- use switchMap to return a timer
return timer(3000).pipe(
takeUntil(events$.pipe(ofType(ValueHasBeenUnset))), // <- unsubscribe from the timer on ValueHasBeenUnset event
map(event => {
return new SomeCommand(); // <- map to SomeCommand once 3 seconds have passed
})
)
})

rxjs: why the stream emit twice when another stream use take(1)

When I use take(1), it will console.log twice 1, like below code:
const a$ = new BehaviorSubject(1).pipe(publishReplay(1), refCount());
a$.pipe(take(1)).subscribe();
a$.subscribe((v) => console.log(v)); // emit twice (1 1)
But when I remove take(1) or remove publishReplay(1), refCount(), it follow my expected (only one 1 console.log).
const a$ = new BehaviorSubject(1).pipe(publishReplay(1), refCount());
a$.subscribe();
a$.subscribe((v) => console.log(v)); // emit 1
// or
const a$ = new BehaviorSubject(1);
a$.pipe(take(1)).subscribe();
a$.subscribe((v) => console.log(v)); // emit 1
Why?
Version: rxjs 6.5.2
Let's first have a look at how publishReplay is defined:
const subject = new ReplaySubject<T>(bufferSize, windowTime, scheduler);
return (source: Observable<T>) => multicast(() => subject, selector!)(source) as ConnectableObservable<R>;
multicast() will return a ConnectableObservable, which is an observable that exposes the connect method. Used in conjunction with refCount, the source will be subscribed when the first subscriber registers and will automatically unsubscribe from the source when there are no more active subscribers. The multicasting behavior is achieved by placing a Subject(or any kind of subject) between the data consumers and the data producer.
() => subject implies that the same subject instance will be used every time the source will be subscribed, which is an important aspect as to why you're getting that behavior.
const src$ = (new BehaviorSubject(1)).pipe(
publishReplay(1), refCount() // 1 1
);
src$.pipe(take(1)).subscribe()
src$.subscribe(console.log)
Let's see what would be the flow of the above snippet:
src$.pipe(take(1)).subscribe()
Since it's the first subscriber, the source(the BehaviorSubject) will be subscribed. When this happens, it will emit 1, which will have to go through the ReplaySubject in use. Then, the subject will pass along that value to its subscribers(e.g take(1)). But because you're using publishReplay(1)(1 indicates the bufferSize), that value will be cached by that subject.
src$.subscribe(console.log)
The way refCount works is that it first subscribes to the Subject in use, and then to the source:
const refCounter = new RefCountSubscriber(subscriber, connectable);
// Subscribe to the subject in use
const subscription = connectable.subscribe(refCounter);
if (!refCounter.closed) {
// Subscribe to the source
(<any> refCounter).connection = connectable.connect();
}
Incidentally, here's what happens on connectable.subscribe:
_subscribe(subscriber: Subscriber<T>) {
return this.getSubject().subscribe(subscriber);
}
Since the subject is a ReplaySubject, it will send the cached values to its newly registered subscriber(hence the first 1). Then, because there were no subscribers before(due to take(1), which completes after the first emission), the source will be unsubscribed again, which should explain why you're getting the second 1.
If you'd like to get only one 1 value, you can achieve this by making sure that every time the source is subscribed, a different subject will be used:
const src$ = (new BehaviorSubject(1)).pipe(
shareReplay({ bufferSize:1, refCount: true }) // 1
);
src$.pipe(take(1)).subscribe()
src$.subscribe(console.log)
StackBlitz.

Can the passage of time be called an Effect?

I know that we can subscribe to actions and depending on what action is dispatched we can dispatch new actions - it can be safely called the Effect, but ...
What if I want to dispatch some actions after the date I keep in state? I care about the good organization of the code in the project, so it is important for me to properly name such a function.
Can the function subcribe for the passage of time and perform actions if time === 0 can be called an Effect?
mySelector
.getTime(this.store)
.pipe(skipWhile(time => time > 0))
.subscribe(data => {
// this.store.dispatch([...]);
});
you can you store in the effects and it can help to solve your issue.
#Injectable()
export class AuthActivationSelfEffects {
public readonly scheduler$: Observable<Action> = createEffect(() =>
this.actions$.pipe(
switchMap(() => this.store.select(getTime)),
skipWhile(time => time > 0),
switchMap(() => [
action1,
action2,
action3,
]),
),
);
constructor(protected readonly store: Store) {}
}
The right way would be to have a start action and an end action which are triggered somewhere from controllers.
Because effects are for side effects, not for scheduling.
it would look like:
.pipe(
ofType(startAction),
delay(5000), // time to wait
takeUntil(this.actions$.pipe(ofType(endAction))), // cancel
switchMap(() => [action1, action2, action3]), // actions to dispatch if it wasn't canceled.
);

Implement a loop logic within an rxjs pipe

I have a class, QueueManager, which manages some queues.
QueueManager offers 3 APIs
deleteQueue(queueName: string): Observable<void>
createQueue(queueName: string): Observable<string>
listQueues(): Observable<string>: Observable`
deleteQueue is a fire-and-forget API, in the sense that it does not return any signal when it has completed its work and deleted the queue. At the same time createQueue fails if a queue with the same name already exists.
listQueues() returns the names of the queues managed by QueueManager.
I need to create a piece of logic which deletes a queue and recreates it. So my idea is to do something like
call the deleteQueue(queueName) method
start a loop calling the listQueues method until the result returned shows that queueName is not there any more
call createQueue(queueName)
I do not think I can use retry or repeat operators since they resubscribe to the source, which in this case would mean to issue the deleteQueue command more than once, which is something I need to avoid.
So what I have thought to do is something like
deleteQueue(queueName).pipe(
map(() => [queueName]),
expand(queuesToDelete => {
return listQueues().pipe(delay(100)) // 100 ms of delay between checks
}),
filter(queues => !queues.includes(queueName)),
first() // to close the stream when the queue to cancel is not present any more in the list
)
This logic seems actually to work, but looks to me a bit clumsy. Is there a more elegant way to address this problem?
The line map(() => [queueName]) is needed because expand also emits values from its source observable, but I don't think that's obvious from just looking at it.
You can use repeat, you just need to subscribe to the listQueues observable, rather than deleteQueue.
I've also put the delay before listQueues, otherwise you're waiting to emit a value that's already returned from the API.
const { timer, concat, operators } = rxjs;
const { tap, delay, filter, first, mapTo, concatMap, repeat } = operators;
const queueName = 'A';
const deleteQueue = (queueName) => timer(100);
const listQueues = () => concat(
timer(1000).pipe(mapTo(['A', 'B'])),
timer(1000).pipe(mapTo(['A', 'B'])),
timer(1000).pipe(mapTo(['B'])),
);
const source = deleteQueue(queueName).pipe(
tap(() => console.log('queue deleted')),
concatMap(() =>
timer(100).pipe(
concatMap(listQueues),
tap(queues => console.log('queues', queues)),
repeat(),
filter(queues => !queues.includes(queueName)),
first()
)
)
);
source.subscribe(x => console.log('next', x), e => console.error(e), () => console.log('complete'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.4/rxjs.umd.js"></script>

Resources