-
Notifications
You must be signed in to change notification settings - Fork 4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
PollMatrix (cancelable polling by endpoint name and parameters) #10
Comments
Greetings! Thanks for trying the library out.
That's certainly possible, but I'd like to learn more about your use-case. It sounds like you need a poll for every individual user. I think this is an important scenario that we should think about how to solve across the board. Since In the mean time, you're free to build your own poll middleware and use that. Linked below is the source https://github.com/neurosnap/saga-query/blob/main/src/middleware.ts#L200 export function poll(parentTimer?: number, cancelType?: string) {
return function* poller(
actionType: string,
saga: any,
...args: any[]
): SagaIterator<void> {
const cancel = cancelType || actionType;
function* fire(action: { type: string }, timer: number) {
while (true) {
yield call(saga, action, ...args);
yield delay(timer);
}
}
while (true) {
const action = yield take(`${actionType}`);
const timer = action.payload?.timer || parentTimer;
yield race([call(fire, action, timer), take(`${cancel}`)]);
}
};
} I'd also be happy to accept any PRs that you do make (like adding What you could do immediately is something like this: while (true) {
const action = yield take(`${actionType}`);
const timer = action.payload?.timer || parentTimer;
const cancelPoll = action.payload?.cancelType || cancel;
yield race([call(fire, action, timer), take(`${cancelPoll}`)]);
} If you have any questions or feedback I'd be happy to listen. |
Thank you, I'll go further with your suggestion and I think that will do it, Anyway I wrote down the use-case. On client-side : 0 - the user selects some ( multiple ) ITEMS to be calculated (ITEMID), by clicking on a tree-view. On the server (Node.js Express Redis) - on poll initial 4 -on request by ITEMID, some possibly heavy selections are made from redis tables having as result a dataset. on client 8 - as long as the ITEM is in process on client-side we poll the server to see if there are any changes in the underlying dataset. on server on poll on client User could select multiple ITEMID or just one. I.E. we subscribe/unsubscribe to a polling by ITEMID request. Ideally we would like to pause the polling and resume. |
It seems that the task isn't trivial at all. Sorry for the irrelevant details above. Looking forward to the ongoing PR. |
Is there anything I can help with? |
For now I implemented this feature using an other approach than saga-query in order to go on with the project (mixing valtio proxy, rtkq and sagas to make the feature fit in the project ). |
Reading your use-case again, it seems like you want to create a poll per ITEMID and be able to cancel the poll per ITEMID, is that right? |
Yes. That is correct. |
I think that problem is that the "name" is untouchable. Initially I thought that cancelling by parameter is enough, but then the next call overlaps and invalidates the first call and so on. |
So I haven't actually tested this code out, but theoretically something like this should work. export function pollMatrix(cancelType: string, parentTimer?: number) {
return function* poller(
actionType: string,
saga: any,
...args: any[]
): SagaIterator<void> {
const pollMap: { [key: string]: Task } = {};
function* fire(action: { type: string }, timer: number) {
while (true) {
yield call(saga, action, ...args);
yield delay(timer);
}
}
function* watcher(): SagaIterator<void> {
while (true) {
const action = yield take(`${actionType}`);
const timer = action.payload?.timer || parentTimer;
const task: Task = yield fork(fire, action, timer);
pollMap[action.payload.key] = task;
}
}
function* canceler(): SagaIterator<void> {
while (true) {
const action = yield take(`${cancelType}`);
const id = action.payload.key
const task = pollMap[id];
if (!task) continue;
yield cancel(task);
delete pollMap[id];
}
}
yield all([call(canceler), call(watcher)]);
};
}
const cancelFetchData =
(payload: { payload: { key: string } }) => ({ type: 'cancel-fetch-data', payload });
const api = createApi();
const pollFetchData = api.get<{ id: string }>(
'/data/:id',
{ saga: pollMatrix(`${cancelFetchData}`) },
api.cache()
)
const Row = ({ itemId }: { itemId: string }) => {
const action = pollFetchData({ id: itemId });
useEffect(() => {
dispatch(action);
}, [itemId]);
const cancel = () => {
dispatch(cancelFetchData(action))
}
return <button onClick={cancel}>cancel: {itemId}</div>
}
const Table = () => {
const items = ['1', '2', '3'];
return <div>items.forEach((itemId) => <Row key={itemId} itemId={itemId} />)</div>
} There's probably a better approach but the perks of this setup is everything is still contained within an endpoint saga. Under the hood when we create an api endpoint with |
Hello and thanks. It works perfectly. interface IWatchParams {
PARAM1_ID: number;
PARAM2_ID: number;
} // for example of multiple params. as it doesn't matter how we define our "ITEM"
const unwatchSome = ({payload:{key}}) => ({ type: 'cancel-fetch-data', payload: {key: key} });
const watchSome = api.post<IWatchParams>(
`endpoint/path`,
{ saga: pollMatrix('cancel-fetch-data', 20 * 1000) },
function* onLoadSaga(ctx: ApiCtx<IReportResponse>, next) {
/// prepare input data ...
ctx.request = ctx.req({
body: JSON.stringify({anyParams: 'anyParams needed in backend'})
});
yield next();
// manage result...;
}
);
export function* registerWatch (params) {
const theAction = yield watchSome(params);
yield put(theAction)
return {
unsubscribe: unwatchSome(theAction)
}
} I needed to call this from another saga, so it went like this: const localMap = {}; // some Object, or Map or Proxy to keep track of registered actions
function* subscribe(id1, id2) {
const {unsubscribe} = yield call(registerWatch, { PARAM1_ID: id1, PARAM2_ID: id2 });
localMap[`${id1}:${id1}`] = yield unsubscribe;
return;
}
// and later
function* unsubscribe(id1, id2) {
if (local.workmap[`${id1}:${id1}`] === undefined) return;
yield put(local.workmap[`${id1}:${id1}`]);
yield delete local.workmap[`${id1}:${id1}`];
} With that I can go further. like |
It would be easier like this: import { SagaIterator, Task } from 'redux-saga';
import { all, delay, call, take, cancel, fork } from 'redux-saga/effects';
export function pollMatrix(cancelType: string, parentTimer?: number) {
return function* poller(
actionType: string,
saga: any,
...args: any[]
): SagaIterator<void> {
const pollMap: { [key: string]: Task } = {};
function* fire(action: { type: string }, timer: number) {
while (true) {
yield call(saga, action, ...args);
yield delay(timer);
}
}
function* watcher(): SagaIterator<void> {
while (true) {
const action = yield take(`${actionType}`);
// so we build a key which can be rebuilt later based on action and params //
const watcherKeyStr = JSON.stringify({name : action.payload.name , ... action.payload.options});
const timer = action.payload?.timer || parentTimer;
const task: Task = yield fork(fire, action, timer);
pollMap[watcherKeyStr] = task;
}
}
function* canceler(): SagaIterator<void> {
while (true) {
const action = yield take(`${cancelType}`);
const watcherKeyStr = action.payload;
const task = pollMap[watcherKeyStr];
if (!task) continue;
yield cancel(task);
delete pollMap[watcherKeyStr];
}
}
yield all([call(canceler), call(watcher)]);
};
} then from api: export const watchSome = api.post<IWatchParams>(
`endpoint/path`,
{ saga: pollMatrix('cancel-fetch-data', 20 * 1000) },
function* onLoadSaga(ctx: ApiCtx<IReportResponse>, next) {
/// prepare input data ...
ctx.request = ctx.req({
body: JSON.stringify({anyParams: 'anyParams needed in backend'})
});
yield next();
// manage result...;
}
);
export function* cancelWatch (params) {
// Here we rebuild the key...
// we need the "name"
// is there a function to get the name out without invoking the api.post ?
const {payload:{name}} = yield watchSome(params);
// or just write-it manually appending [POST] (IS IT SAFE ?)
// anyway it will cancel the poll...
yield put({ type: 'cancel-fetch-data', payload: JSON.stringify({name: name, ...params})});
} then from the front (in my case another saga) //register
yield put(watchSome ({ GROUPID: GROUPID, ITEMID: id })); // or whatever params we use
//is important that the params are well typed to keep the key consistency ( e.g "ID":1 vs "ID": "1") when stringifying.
// unregister
yield call(cancelWatch, { GROUPID: GROUPID, ITEMID: id}); |
Perhaps it would be more elegant just to reuse the "key" parameter from api based on actionType and options and keep it in the pollMap. But not knowing the mechanics or that key I didn't went on that direction. |
https://github.com/neurosnap/saga-query/blob/main/src/pipe.ts#L146 We serialize the name of the action as well as the options you send to the action as a json encoded, base64 string. We use this key for automatic caching and the like. |
Thanks a lot for this. I feel stupid for trying to reinvent the wheel. It totally make sense the first approach of yours. I just missed it. import { SagaIterator, Task } from 'redux-saga';
import { all, delay, call, take, cancel, fork } from 'redux-saga/effects';
export function pollMatrix(cancelType: string, parentTimer?: number) {
return function* poller(
actionType: string,
saga: any,
...args: any[]
): SagaIterator<void> {
const pollMap: { [key: string]: Task } = {};
function* fire(action: { type: string }, timer: number) {
while (true) {
yield call(saga, action, ...args);
yield delay(timer);
}
}
function* watcher(): SagaIterator<void> {
while (true) {
const action = yield take(`${actionType}`);
const timer = action.payload?.timer || parentTimer;
const task: Task = yield fork(fire, action, timer);
pollMap[action.payload.key]=task;
}
}
function* canceler(): SagaIterator<void> {
while (true) {
const action = yield take(`${cancelType}`);
const watcherKeyStr = action.payload;
const task = pollMap[watcherKeyStr];
if (!task) continue;
yield cancel(task);
delete pollMap[watcherKeyStr];
}
}
yield all([call(canceler), call(watcher)]);
};
} the api const watchWorkMap = api.post<IWorkMapParams>(
`reportshead/wmap`,
{ saga: pollMatrix('cancel-fetch-data', 20 * 1000) },
function* onLoadWorkMap(ctx: ApiCtx<IReportResponse>, next) {
//...
});
export function* workMapPolling( exec: 'subscribe'|'unsubscribe', params: IWorkMapParams) {
const theAction = yield watchWorkMap(params);
if (exec === 'subscribe') {
yield put(theAction);
}
if (exec === 'unsubscribe') {
const {payload:{key}} = yield theAction;
yield put({ type: 'cancel-fetch-data', payload:key});
}
return ;
} the consumer: import { workMapPolling } from '../apis';
//..
yield call(workMapPolling, 'subscribe',{/*params */});
//OR
yield call(workMapPolling,'unsubscribe', {/*params */}); Perhaps the pollMatrix should land into the package as a standard option. Thanks again for your support! |
I refactored a little and put some sugar. Please take a look at this forked sandbox I have concern though. Can we be sure that the key will be the same if the user set the parameters in different order? As I tested it works ok as the order of the keys is somehow managed by the browser. |
I think you are most certainly right. We should sort the order of the keys to ensure it's always the same order. I'd be delighted to accept a PR for that change. If not, I'll get to it sometime next week. |
I'll be happy to contribute. I'll get on with this. |
Hello, congrats and thanks for the great work.
I wonder if is possible to somehow pass the cancelType string for polling from the dispatch ?
like
dispatch(loadSomeWork({userID:5},{poll:30*1000, cancelType: userId5 })
?Or from within the api definition, or some workaround to be able to set distinct polling requests (e.g. byUserId) and cancel individually by the parameters, on the same endpoint.
Thanks, Vlad
The text was updated successfully, but these errors were encountered: