My app has a modal with a spinner that's displayed whenever a long blocking action is taking place.
There's several of these long blocking actions, each with an action that marks its start and finish.
Given the "stream of actions", whenever one of the start action is dispatched, I want to dispatch the showWaitingIndication action until the corresponding end action is dispatched and then dispatch hideWaitingIndication. If another start action is dispatched and then it's corresponding end action is dispatched while the first blocking action is in progress, it shouldn't call showWaitingIndication again or hideWaitingIndication. Nor should hideWaitingIndication be dispatched while an action is still active.
Basically the idea is that as long as a blocking action is active, the waiting indication shouldn't hide.
e.g.
StartA -> dispatch(showWaitingIndication) -> other events -> endA -> dispatch(hideWaitingIndication)
StartA -> dispatch(showWaitingIndication) -> startB -> endB (shouldn't call hide) -> endA -> dispatch(hideWaitingIndication)
Also
StartA -> dispatch(showWaitingIndication) -> startB -> endA (shouldn't call hide!) -> endB -> dispatch(hideWaitingIndication)
I'm trying to wrap my head around how to implement this with streams (which I strongly believe are a good fit for this issue).
So far I've come up with something like this (which doesn't work)
let showHideActionPairs = getShowHideActionPairs(); // { "startA": "endA", "startB": "endB"}
let showActions = Object.keys(showHideActionPairs);
return action$ => action$.pipe(
filter(action => Object.keys(showHideActionPairs).includes(action.type)),
switchMap(val =>
{
let hideAction = showHideActionPairs[val.type];
return concat(
of(waitingIndicationShowAction),
empty().pipe(
ofType(hideAction),
mapTo(waitingIndicationHideAction)
))
}
)
);
What's the proper way of doing this?
This is a very interesting problem!
I think you could try this:
const showHideActionPairs = getShowHideActionPairs(); // { "startA": "endA", "startB": "endB"}
actions$.pipe(
windowWhen(() => actions$.pipe(filter(action => action.type === hideWaitingIndication))),
mergeMap(
window => window.pipe(
mergeMap(
action => someAsyncCall().pipe(
mapTo(showHideActionPairs[action]),
startWith(showHideActionPairs[action])
)
),
scan((acc, crtEndAction) => {
// first time receiving this end action -> the beginning of the async call
if (!(crtEndAction in acc)) {
acc[crtEndAction] = true;
return acc;
}
// if the `crtEndAction` exists, it means that the async call has finished
const {[crtEndAction]: _, ...rest} = acc;
return rest;
}, Object.create(null)),
filter(obj => Object.keys(obj).length === 0),
mapTo(hideWaitingIndication),
// a new window marks the beginning of the modal
startWith(showWaitingIndication),
)
)
)
My first thought was that I need to a find a way to represent a chain of events, such that the chain starts at showWaitingIndication and ends with hideWaitingIndication. The end of the chain is actually indicated by the last completed async call(end{N}). So I considered it would be a good use case for windowWhen.
But what is a window ? A window is nothing more than a Subject:
/* ... */
const window = this.window = new Subject<T>();
this.destination.next(window);
/* ... */
The way windowWhen(() => closeNotifier) works is that it will send a Subject(a window) as a next value(that's why we have mergeMap(window => ...)) and it will push values(e.g actions) through it. We're accessing these values inside window.pipe(...). When closeNotifier emits, the current window will complete and a new window will be created and passed along, so that subsequent actions will be sent through it. It's worth noting that a window is created by default when the stream is subscribed:
constructor(protected destination: Subscriber<Observable<T>>,
private closingSelector: () => Observable<any>) {
super(destination);
this.openWindow(); // !
}
Let's say that a we're receiving the first action in the current window.
mergeMap(
action => someAsyncCall().pipe(
mapTo(showHideActionPairs[action]),
startWith(showHideActionPairs[action])
)
),
As soon as the action is intercepted, we'll send its expected end value, so that it can be stored in the scan's accumulator. When that async call of that action would be finished, it will send again that end value, so that it can be removed from the accumulator.
This way, we can determine the lifespan of a window, which will be closed when there are no more end values in the accumulator.
When this happens
filter(obj => Object.keys(obj).length === 0),
mapTo(hideWaitingIndication),
we make sure that we notify that all the actions have finished their task.
I've accepted Andrei's answer, as he was to one to point me out in the right direction, and his solution involving a windowWhen and accumulator was the right mind frame to tackle this issue. I'm also posting my own solution based on his for completeness' sake, as I feel the logic here is more explicit (and personally was easier for me to wrap my head around as I was searching for the solution):
let showHideActionPairs = getShowHideActionPairs();
const relevantActionsTypesArray = Object.keys(showHideActionPairs).concat(Object.values(showHideActionPairs));
actions$ => actions$.pipe(
// close the "window" when a hide action is received
windowWhen(() => actions$.pipe(ofType(waitingIndicationHideActionName),)),
mergeMap(
window => window.pipe(
// filter to only look at start/end actions
ofType.apply(null, relevantActionsTypesArray),
scan((accumulator, action) => {
let waitingForEndAction = "startAction" in accumulator;
// first time we see a start action
if (!waitingForEndAction && action.type in showHideActionPairs) {
accumulator.startAction = action.type;
accumulator.actionable = true;
// found the right end action
} else if (waitingForEndAction && action.type === showHideActionPairs[accumulator.startAction]) {
accumulator.endAction = action.type;
accumulator.actionable = true;
// any other case is not actionable (will not translate to to an action)
} else {
accumulator.actionable = false;
}
return accumulator;
}, {}),
// accumulator spits out stuff for every action but we only care about the actionables
filter(obj => obj.actionable),
map(obj => {
if (obj.endAction){
return waitingIndicationHideAction
} else if (obj.startAction) {
return waitingIndicationShowAction
}
}),
)
)
)
};
Related
My situation is as follows: I am performing sequential HTTP requests, where one HTTP request depends on the response of the previous. I would like to combine the response data of all these HTTP requests into one observable. I have implemented this before using an async generator. The code for this was relatively simple:
async function* AsyncGeneratorVersion() {
let moreItems = true; // whether there is a next page
let lastAssetId: string | undefined = undefined; // used for pagination
while (moreItems) {
// fetch current batch (this performs the HTTP request)
const batch = await this.getBatch(/* arguments */, lastAssetId);
moreItems = batch.more_items;
lastAssetId = batch.last_assetid;
yield* batch.getSteamItemsWithDescription();
}
}
I am trying to move away from async generators, and towards RxJs Observables. My best (and working) attempt is as follows:
const observerVersion = new Observable<SteamItem>((subscriber) => {
(async () => {
let moreItems = true;
let lastAssetId: string | undefined = undefined;
while (moreItems) {
// fetch current batch (this performs the HTTP request)
const batch = await this.getBatch(/* arguments */, lastAssetId);
moreItems = batch.more_items;
lastAssetId = batch.last_assetid;
const items = batch.getSteamItemsWithDescription();
for (const item of items) subscriber.next(item);
}
subscriber.complete();
})();
});
Now, I believe that there must be some way of improving this Observer variant - this code does not seem very reactive to me. I have tried several things using pipe, however unfortunately these were all unsuccessful.
I found concatMap to come close to a solution. This allowed me to concat the next HTTP request as an observable (done with the this.getBatch method), however I could not find a good way to also not abandon the response of the current HTTP request.
How can this be achieved? In short I believe this problem could be described as appending data to an observable inside the observable itself. (But perhaps this is not a good way of handling this situation)
TLDR;
Here's a working StackBlitz demo.
Explanation
Here would be my approach:
// Faking an actual request
const makeReq = (prevArg, response) =>
new Promise((r) => {
console.log(`Running promise with the prev arg as: ${prevArg}!`);
setTimeout(r, 1000, { prevArg, response });
});
// Preparing the sequential requests.
const args = [1, 2, 3, 4, 5];
from(args)
.pipe(
// Running the reuqests sequantially.
mergeScan(
(acc, crtVal) => {
// `acc?.response` will refer to the previous response
// and we're using it for the next request.
return makeReq(acc?.response, crtVal);
},
// The seed(works the same as `reduce`).
null,
// Making sure that only one request is run at a time.
1
),
// Combining all the responses into one object
// and emitting it after all the requests are done.
reduce((acc, val, idx) => ({ ...acc, [`request${idx + 1}`]: val }), {})
)
.subscribe(console.warn);
Firstly, from(array) will emit each item from the array, synchronously and one by one.
Then, there is mergeScan. It is exactly the result of combining scan and merge. With scan, we can accumulate values(in this case we're using it to have access to the response of the previous request) and what merge does is to allow us to use observables.
To make things a bit easier to understand, think of the Array.prototype.reduce function. It looks something like this:
[].reduce((acc, value) => { return { ...acc }}, /* Seed value */{});
What merge does in mergeScan is to allow us to use the accumulator something like (acc, value) => new Observable(...) instead of return { ...acc }. The latter indicates a synchronous behavior, whereas with the former we can have asynchronous behavior.
Let's go a bit step by step:
when 1 is emitted, makeReq(undefined, 1) will be invoked
after the first makeReq(from above) resolves, makeReq(1, 2) will be invoked
after makeReq(1, 2) resolves, makeReq(2, 3) will be invoked and so on...
Somebody I consulted regarding this matter came up with this solution, I think it's quite elegant:
defer(() => this.getBatch(options)).pipe(
expand(({ more_items, last_assetid }) =>
more_items
? this.getBatch({ ...options, startAssetId: last_assetid })
: EMPTY,
),
concatMap((batch) => batch.getSteamItemsWithDescription()),
);
From my understanding the use of expand here is very similar to the use of mergeScan in #Andrei's answer
So I'm relatively inexperienced with rxjs so if this is something that would be a pain or really awkward to do, please tell me and I'll go a different route. So in this particular use case, I was to queue up updates to send to the server, but if there's an update "in flight" I want to only keep the latest item which will be sent when the current in flight request completes.
I am kind of at a loss of where to start honestly. It seems like this would be either a buffer type operator and/or a concat map.
Here's what I would expect to happen:
const updateQueue$ = new Subject<ISettings>()
function sendToServer (settings: ISettings): Observable {...}
...
// we should send this immediately because there's nothing in-flight
updateQueue$.next({ volume: 25 });
updateQueue$.next({ volume: 30 });
updateQueue$.next({ volume: 50 });
updateQueue$.next({ volume: 65 });
// lets assume that our our original update just completed
// I would now expect a new request to go out with `{ volume: 65 }` and the previous two to be ignored.
I think you can achieve what you want with this:
const allowNext$ = new Subject<boolean>()
const updateQueue$ = new Subject<ISettings>()
function sendToServer (settings: ISettings): Observable { ... }
updateQueue$
.pipe(
// Pass along flag to mark the first emitted value
map((value, index) => {
const isFirstValue = index === 0
return { value, isFirstValue }
}),
// Allow the first value through immediately
// Debounce the rest until subject emits
debounce(({ isFirstValue }) => isFirstValue ? of(true) : allowNext$),
// Send network request
switchMap(({ value }) => sendToServer(value)),
// Push to subject to allow next debounced value through
tap(() => allowNext$.next(true))
)
.subscribe(response => {
...
})
This is a pretty interesting question.
If you did not have the requirement of issuing the last in the queue, but simply ignoring all requests of update until the one on the fly completes, than you would simply have to use exhaustMap operator.
But the fact that you want to ignore all BUT the last request for update makes the potential solution a bit more complex.
If I understand the problem well, I would proceed as follows.
First of all I would define 2 Subjects, one that emits the values for the update operation (i.e. the one you have already defined) and one dedicated to emit only the last one in the queue if there is one.
The code would look like this
let lastUpdate: ISettings;
const _updateQueue$ = new Subject<ISettings>();
const updateQueue$ = _updateQueue$
.asObservable()
.pipe(tap(settings => (lastUpdate = settings)));
const _lastUpdate$ = new Subject<ISettings>();
const lastUpdate$ = _lastUpdate$.asObservable().pipe(
tap(() => (lastUpdate = null)),
delay(0)
);
Then I would merge the 2 Observables to obtain the stream you are looking for, like this
merge(updateQueue$, lastUpdate$)
.pipe(
exhaustMap(settings => sendToServer(settings))
)
.subscribe({
next: res => {
// do something with the response
if (lastUpdate) {
// emit only if there is a new "last one" in the queue
_lastUpdate$.next(lastUpdate);
}
},
});
You may notice that the variable lastUpdate is used to control that the last update in the queue is used only once.
I have an input element, where I want to show an autocomplete solution. I try to control the HTTP request's number with RxJS. I want to do: the HTTP request start only after 1 second of the user stop the typing.
I have this code:
of(this.inputBox.nativeElement.value)
.pipe(
take(1),
map((e: any) => this.inputBox.nativeElement.value),
// wait 1 second to start
debounceTime(1000),
// if value is the same, ignore
distinctUntilChanged(),
// start connection
switchMap(term => this.autocompleteService.search({
term: this.inputBox.nativeElement.value
})),
).subscribe((result: AutocompleteResult[]) => {
console.log(result);
});
The problem is the debounceTime(1000) didn't wait to continue the pipe. The switchMap start immediately after every keyup event.
How can I wait 1 second after the user finishes typing?
The problem is that your chain starts with of(this.inputBox.nativeElement.value).pipe(take(1), ...) so it looks like you're creating a new chain (with a new debounce timer) on every single key press.
Instead you should have just one chain and push values to its source:
const keyPress$ = new Subject();
...
keyPress$
.pipe(
debounceTime(1000),
...
)
...
keyPress$.next(this.inputBox.nativeElement.value);
Why don't you create a stream with fromEvent?
I didn't find necessary to use distinctUntiChanged as the input event only triggers when a change occurs (i.e. the user adds/removes stuff). So text going through the stream is always different.
const {fromEvent} = rxjs;
const {debounceTime, map} = rxjs.operators;
const text$ =
fromEvent(document.querySelector('input'), 'input')
.pipe(
debounceTime(1000),
map(ev => ev.target.value));
text$.subscribe(txt => {
console.log(txt);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.4/rxjs.umd.min.js"></script>
<input/>
I am writing action creator in react app. where in when i do some api call i need to show the Progress Loader on screen. So, my action creator looks like this.
export const fetchData = (actionType, param) => (dispatch) => {
dispatch(Action(ActionConstants.SHOW_PROGRESS_LOADER)); // Show Loader Action
return fetchDataRequest(actionType, param) // Here is Fetch APi Call
.then(responseData => {
dispatch(Action(ActionConstants.HIDE_PROGRESS_LOADER));
dispatch(Action(recd(actionType), { data: responseData, receivedAt: Date.now() }));
}).catch((error) => {
dispatch(Action(ActionConstants.HIDE_PROGRESS_LOADER)); // Hide Loader Action
});
};
When i write this piece of code its working as expected, i am dispatching the action as dispatch(fetchData(data)) from component and i am able to show the loader in my Parent Component. What i understand is fetch is returning me the promise. Once the fetch gets completed then i am hiding the loader which is working as expected.
Now, There is scenario where in i need to do some validation where in i don't have to make any api call but all the validation are performed locally.
Here also i want to do the same thing like i need to show loader in my parent component as well when all the validation are done i need to hide the loader.
I have written the same piece of code even actions are getting called but my render function is not getting called.
My Code Looks like:
// This my action creator which will actually do the validation
export const validateAndSaveData = () => {
return ((dispatch, getState) => {
return new Promise((resolve, reject) => {
let saveRecommendDetailsFlag = true;
// here i am dispacthing some action and storing data in my store
saveRecommendDetailsFlag = canSaveData(getState());
if (saveRecommendDetailsFlag) {
resolve('SUCCESS');
} else {
reject('ERROR');
}
});
});
};
And there is one more action creator which i am calling it from from UI Component which will first initiate the show loader action and then perform validation and based on the result of validation i have to hide the loader.
export const saveData = () => {
return ((dispatch) => {
dispatch(Action(ActionConstants.SHOW_PROGRESS_LOADER)); // Show Loader Action
return dispatch(validateAndSaveData())
.then(() => {
// Here i m dispatching an action to do some more processing.
dispatch(Action(ActionConstants.HIDE_PROGRESS_LOADER)); // Hide Loader Action
})
.catch(() => {
dispatch(Action(ActionConstants.HIDE_PROGRESS_LOADER)); // Hide Loader Action
});
});
};
Everything is working fine but my loader are not coming on the screen. i am not able to figure it out where am i doing wrong.
Can anyone suggest something how can i solve this issue?
I got some workaround using setTimeout func but i don't think that is right approach.
export const saveData = () => {
return ((dispatch) => {
dispatch(Action(ActionConstants.SHOW_PROGRESS_LOADER)); // Show Loader Action
setTimeout(()=>return dispatch(validateAndSaveData())
.then(() => {
// Here i m dispatching an action to do some more processing.
dispatch(Action(ActionConstants.HIDE_PROGRESS_LOADER)); // Hide Loader Action
})
.catch(() => {
dispatch(Action(ActionConstants.HIDE_PROGRESS_LOADER)); // Hide Loader Action
});
},10);
});
};
Your code looks reasonable, my suspicion is that your validateAndSaveData promise finishes so quickly that there is no visible loader on the screen.
In that case, a timeout is totally reasonable. However, in order to do it properly, I would keep a state on if the loading screen is visible + if it's been shown long enough. You can then remove the loading screen once it is both up for long enough, and the actual event expires.
I'm not sure which action package you're using, so I can't post exact code, but the pseudocode would look something like this:
const delay = (seconds) => new Promise((resolve) => setTimeout(resolve, seconds));
let loadingCounter = 0;
const showLoadingScreen = () => (dispatch) => {
const counter = loadingCounter;
loadingCounter++;
delay(5).then(() => {
if (getStore().loadingScreen.counter === counter) {
dispatch(Action(ActionConstants.PROGRESS_LOADER_DELAY_ELAPSED))
}
})
return dispatch(Action(ActionConstants.SHOW_PROGRESS_LOADER, counter))
}
Basically, you would keep track of 3 pieces of state for the loader:
{
counter: 0,
taskCompleted: false,
canHide: false,
}
Counter is saved so that you can disambiguate what happens if you get SHOW_PROGRESS_LOADER while an existing SHOW_PROGRESS_LOADER is in progress.
taskCompleted keeps a record of whether the thing you're waiting on is done, and canHide keeps track if the loader has been visible on the screen long enough.
When you dispatch PROGRESS_LOADER_DELAY_ELAPSED it sets canHide to true, and when you dispatch HIDE_PROGRESS_LOADER it sets taskCompleted to true. (although you may want to rename the latter action). When both canHide and taskCompleted are set to true, only then can the loader go away.
This is a pretty common UI pattern - Try to complete a task quickly. If it takes more than a short amount of time, then throw up a loading dialog. However, the loading dialog is guaranteed to stay up a minimum amount of time to prevent flickering. So the more advanced version of this kind of pattern would be to add another state which doesn't show the progress loader at all unless the call takes more than Y milliseconds.
Hope this makes sense, leave a comment if not :-)
Cancelling from the consumer side, might be called using takeUntil, but that's not necessarily very dynamic. In this case, though, I am looking to cancel an Observable from the producer side of the equation, in the same way you might wish to cancel a Promise inside a promise chain (which is not very possible with the native utility).
Say I have this Observable being returned from a method. (This Queue library is a simple persistent queue that read/writes to a text file, we need to lock read/writes so nothing gets corrupted).
Queue.prototype.readUnique = function () {
var ret = null;
var lockAcquired = false;
return this.obsEnqueue
.flatMap(() => acquireLock(this))
.flatMap(() => {
lockAcquired = true;
return removeOneLine(this)
})
.flatMap(val => {
ret = val; // this is not very good
return releaseLock(this);
})
.map(() => {
return JSON.parse(ret);
})
.catch(e => {
if (lockAcquired) {
return releaseLock(this);
}
else {
return genericObservable();
}
});
};
I have 2 different questions -
If I cannot acquire the lock, how can I "cancel" the observable, to just send back an empty Observable with no result(s)? Would I really have to do if/else logic in each return call to decide whether the current chain is cancelled and if so, return an empty Observable? By empty, I mean an Observable that simple fires onNext/onComplete without any possibility for errors and without any values for onNext. Technically, I don't think that's an empty Observable, so I am looking for what that is really called, if it exists.
If you look at this particular sequence of code:
.flatMap(() => acquireLock(this))
.flatMap(() => {
lockAcquired = true;
return removeOneLine(this)
})
.flatMap(val => {
ret = val;
return releaseLock(this);
})
.map(() => {
return JSON.parse(ret);
})
what I am doing is storing a reference to ret at the top of the method and then referencing it again a step later. What I am looking for is a way to pass the value fired from removeOneLine() to JSON.parse(), without having to set some state outside the chain (which is simply inelegant).
According to your definition of cancel, it is to prevent an observable from sending a value downstream. To prevent an observable from pushing a value, you can use filter:
It can be as simple as:
observable.filter(_ => lockAcquired)
This will only send a notification downstream if lockAcquired is true.
1) It depends on how your method acquireLock works - but I am assuming that it throws an error if it cannot acquire the lock, in that case you could create your stream with a catch and set the fallback stream to an empty one:
return Rx.Observable.catch(
removeLine$,
Rx.Observable.empty()
);
2) To spare the stateful external variable you could simply chain a mapTo:
let removeLine$ = acquireLock(this)
.flatMap(() => this.obsEnqueue
.flatMap(() => removeOneLine(this))
.flatMap(val => releaseLock(this).mapTo(val))
.map(val => JSON.parse(val))
.catch(() => releaseLock(this))
);