Логика в redux-saga

Ожидаемое поведение: идут запросы к апи с интервалом в 1.5 секунды пока не будет успешно выполненного запроса, или вызов экшена sendMessageCancel с нужным id.
Что имеется: после вызова экшена стейт меняется, но запросы продолжают идти с интервалом в 1.5 секунды
P.S. логи после выполнения race тоже не вызываются

Саги:

import {takeEvery, delay, call, put, race, take} from 'redux-saga/effects';
import axios, {AxiosResponse} from 'axios';

import {sendMessageStart, sendMessageSuccess, sendMessageProgress, sendMessageCancel, ISendMessage} from './slice';
import sendMessageAPI from '../../utils/api/sendMessageAPI';
import {ISendMessageResponse} from '../../interfaces/Responses/chat/ISendMessageResponse';


function* sendMessageSaga({payload}: ReturnType<typeof sendMessageStart>) {
    const {token, cancel} = axios.CancelToken.source();

    while (true) {
        try {
            let {action, api} = yield race({
                action: take(action => {
                    return action.type == sendMessageCancel.type && action.payload == payload._id
                }),
                api: call(sendMessageAPI.send, payload, token, function* (progress: number) {
                    yield put(sendMessageProgress({message: payload._id, progress}));
                })
            });

            const resp = api as AxiosResponse<ISendMessageResponse>;

            console.log(resp);
            console.log(action);

            if (resp)
                yield put(sendMessageSuccess(payload._id));
            else {
                console.log('Cancel');
                return cancel();
            }

            break;
        } catch (e) {
            console.log(e);
            yield delay(1500);
        }
    }
}

export default function* sendMessageWatchSaga() {
    yield takeEvery(sendMessageStart.type, sendMessageSaga);
}

API

    import axios, {CancelToken, CancelTokenSource} from 'axios';

import {ISendMessage} from '../../redux/sendMessage/slice';
import {ISendMessageResponse} from '../../interfaces/Responses/chat/ISendMessageResponse';


const client = axios.create({
    baseURL: process.env.API_URL || 'http://localhost:5000'
});

const sendMessageAPI = {
    async send(message: ISendMessage, cancelToken: CancelToken, callback: (progress: number) => void){
        return client.post<ISendMessageResponse>('/messages', message, {
            headers: {Authorization: `Bearer ${localStorage.getItem('token')}`},
            cancelToken: cancelToken,
            onUploadProgress: progressEvent => {
                callback(progressEvent.loaded/progressEvent.total);
            }
        });
    }
};

export default sendMessageAPI;

Slice

import {createSlice, PayloadAction} from '@reduxjs/toolkit';

import {RootState} from '../index';
import {IMessage} from '../../interfaces/IMessage';


//create initial state
export type ISendMessage = {file?: File} & IMessage

type ISendMessageItem = {
    progress: number,
    msg: ISendMessage
};

type ISendMessageState = Record<string, ISendMessageItem>;

const initialState: ISendMessageState = {};

//create slice
const sendMessageSlice = createSlice({
    name: 'sendMessage',
    initialState,
    reducers: {
        start(state, action: PayloadAction<ISendMessage>){
            state[action.payload._id] = {
                progress: 0,
                msg: action.payload
            };
        },
        success(state, action: PayloadAction<string>){
            delete state[action.payload];
        },
        progress(state, action: PayloadAction<{message: string, progress: number}>){
            state[action.payload.message].progress = action.payload.progress;
        },
        cancel(state, action: PayloadAction<string>){
            delete state[action.payload];
        }
    }
});

//selectors
export const selectSendMessageState = (state: RootState) => state.sendMessage;
export const selectSendMessage = (id: string) => (state: RootState) => selectSendMessageState(state)[id];
export const selectSendMessagesForDialog = (id: string) =>
    (state: RootState) => Object.values(selectSendMessageState(state)).filter(it => it.msg.dialog._id == id);

//exports
export const {
    start: sendMessageStart, success: sendMessageSuccess,
    progress: sendMessageProgress, cancel: sendMessageCancel
} = sendMessageSlice.actions;

export default sendMessageSlice.reducer;

Ответы (1 шт):

Автор решения: HTMLProgrammer

Проблема оказалась банальна, вызов апи и последующая задержка занимали поток саги, и она просто игнорировала вызов экшена sendMessageCancel, поэтому я использовал fork и всё заработало

import {takeEvery, delay, call, put, fork, take, cancel, cancelled, race, select} from 'redux-saga/effects';
import axios, {AxiosResponse, CancelToken} from 'axios';

import {ISendMessageResponse} from '../../interfaces/Responses/chat/ISendMessageResponse';
import {sendMessageStart, sendMessageSuccess, sendMessageProgress, sendMessageCancel, ISendMessage} from './slice';
import {messagesAdd} from '../messages';
import {dialogsAdd} from '../dialogs';
import {chatMessagesAdd} from '../chat/messages/slice';
import sendMessageAPI from '../../utils/api/sendMessageAPI';
import {selectChatDialogState} from '../chat/dialog/slice';


function* sendMessageApi(data: ISendMessage, token: CancelToken){
    let i = 0;

    while (true) {
        try {
            //make api call
            const resp: AxiosResponse<ISendMessageResponse> = yield call(sendMessageAPI.send,
                data, token, function* (progress: number) {
                    yield put(sendMessageProgress({message: data._id, progress}));
                });

            //get current dialog
            const {dialog} = yield select(selectChatDialogState);

            //update state
            yield put(sendMessageSuccess(data._id));
            yield put(messagesAdd({
                ...resp.data.newMessage,
                author: resp.data.newMessage.author._id
            } as any));

            //update dialog last message
            yield put(dialogsAdd({_id: data.dialog._id, lastMessage: resp.data.newMessage._id} as any));

            if(dialog == data.dialog._id)
                yield put(chatMessagesAdd({
                    message: resp.data.newMessage._id,
                    first: true
                }));

            //exit loop
            break;
        } catch (e) {
            //exit on task cancel
            if(yield cancelled())
                return;

            //increase interval
            if(i < 60)
                i++;

            yield delay(1000 * i);
        }
    }
}

function* sendMessageSaga({payload}: ReturnType<typeof sendMessageStart>) {
    const source = axios.CancelToken.source(),
        //start send messages
        task = yield fork(sendMessageApi, payload, source.token);

    //start race between success sending and cancellation
    const {api, cancelMessage} = yield race({
        api: take(action => action.type == sendMessageSuccess.type && action.payload == payload._id),
        cancelMessage: take(action => action.type == sendMessageCancel.type && action.payload == payload._id)
    });

    if(cancelMessage){
        //stop api calls
        yield cancel(task);
        source.cancel();
    }
}

export default function* sendMessageWatchSaga() {
    yield takeEvery(sendMessageStart.type, sendMessageSaga);
}
→ Ссылка