Given the example of a download manager. There can be any number of active downloads.
Actions can be dispatched to start, stop, mark completion and mark the download progress of any particular download.
const START_DOWNLOAD = "START_DOWNLOAD";
const startDownload = payload => ({ type: START_DOWNLOAD, payload });
const DOWNLOAD_PROGRESS = "DOWNLOAD_PROGRESS";
const downloadProgress = payload => ({ type: DOWNLOAD_PROGRESS, payload });
const STOP_DOWNLOAD = "STOP_DOWNLOAD";
const stopDownload = payload => ({ type: STOP_DOWNLOAD, payload });
const COMPLETE_DOWNLOAD = "COMPLETE_DOWNLOAD";
const completeDownload = payload => ({ type: COMPLETE_DOWNLOAD payload });
These actions will contain an id to identify the download and can modify the redux state using the following reducer:
const downloadReducer = (state = initialState, action) => {
switch (action.type) {
case STOP_DOWNLOAD:
return {
...state,
[action.payload.id]: {
state: "IDLE",
},
};
case START_DOWNLOAD:
return {
...state,
[action.payload.id]: {
state: "IN_PROGRESS",
progress: 0,
},
};
case DOWNLOAD_PROGRESS:
return {
...state,
[action.payload.id]: {
state: "IN_PROGRESS",
progress: action.payload.progress,
},
};
case COMPLETE_DOWNLOAD:
return {
...state,
[action.payload.id]: {
state: "DONE",
progress: 100,
},
};
default:
return state;
}
};
Now comes the issue on how to manage the async dispatch of these actions using redux observable.
For example we could do something like this:
const downloadEpic = action$ =>
action$.ofType(START_DOWNLOAD).mergeMap(action =>
downloader
.takeUntil(
action$.filter(
stop =>
stop.type === STOP_DOWNLOAD &&
stop.payload.id === action.payload.id,
),
)
.map(progress => {
if (progress === 100) {
return completeDownload({
id: action.payload.id
});
} else {
return downloadProgress({
id: action.payload.id,
progress
});
}
}),
);
This works. However, what if we want to limit the number of active downloads that are allowed. We can replace mergeMap
with concatMap
to only allow one active download at a time. Or we can supply the concurrent parameter to mergeMap
and specify exactly how many executions of the inner downloader observable we want to allow at once.
However, this comes with the problem that we now cannot stop queued downloads.
I have created a complete working example that you can try here.
How can you limit and queue downloads using rxjs and redux observable in the most idiomatic way possible?
I've been eyeing this one for a couple days but haven't had the time to give you a solid answer with complete code because it's relatively involved. So instead I'll just give you the tl;dr version that's better than nothing I hope :)
My gut tells me that I would have the UI dispatch an action that represents an attempt to download rather than a true guarantee. e.g. ATTEMPT_DOWNLOAD
. An epic would listen for this action and check if the current number of active downloads exceeds is >= the max and if so, would emit an action to enqueue the download instead of starting it.
Your reducers would store the download IDs of those active and download IDs of those queued up. e.g.
{ active: ['123', '456', ...etc], queued: ['789'] }
Which you would use for both keeping track of active/queued specifically but also to know the counts of them active.length
, queued.length
When a download completes an epic somewhere would check if there are any queued up downloads and if yes, dequeue one. Exactly how you do that is mostly personal preference. e.g. if the epic emits DEQUEUE_DOWNLOAD
or whatever.
If a cancellation action is dispatched your reducers would need to look in both active
and queued
and remove the ID if it exists in there. If it was in fact active and not just queued then the epic which is handling the download would be listening for the cancellation action and it would stop it and do the same dequeue check as above.
This is a bit hand-wavy, but the big takeaways are these:
START_DOWNLOAD
It's not always clear where business logic should lie. e.g. should the reducers just be mostly getters/setters or should they have more opinionated logic and make decisions? There aren't a lot of rules in these cases. Just try to be consistent.
btw this is entirely from my gut. I might have found (or you might still find) that in practice this isn't a good solution. Just giving my initial thoughts!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With