Standard way of reconnecting to webSocket server in redux-Saga? - reactjs

I'm trying to connect from my react App to websocket server using redux-saga and want to capture the connection loss (server error, reboot) so that to reconnect say in intervals of 4 seconds until the connection is again back. The problem is on reconnecting to webSocket the redux store does not get updated anymore.
I tried using eventChannel of redux-saga as per following code. Unfortunately there was not or at least I couldn't find any documentation answering ws reconnect in redux-saga.
import {eventChannel} from 'redux-saga';
import {all, takeEvery, put, call, take, fork} from 'redux-saga/effects'
import {INITIALIZE_WS_CHANNEL} from "../../constants/ActionTypes"
import {updateMarketData} from "../actions"
function createEventChannel() {
return eventChannel(emit => {
//Subscribe to websocket
const ws = new WebSocket('ws://localhost:9000/rates');
ws.onopen = () => {
console.log("Opening Websocket");
};
ws.onerror = error => {
console.log("ERROR: ", error);
};
ws.onmessage = e => {
return emit({data: JSON.parse(e.data)})
};
ws.onclose = e => {
if (e.code === 1005) {
console.log("WebSocket: closed");
} else {
console.log('Socket is closed Unexpectedly. Reconnect will be attempted in 4 second.', e.reason);
setTimeout(() => {
createEventChannel();
}, 4000);
}
};
return () => {
console.log("Closing Websocket");
ws.close();
};
});
}
function * initializeWebSocketsChannel() {
const channel = yield call(createEventChannel);
while (true) {
const {data} = yield take(channel);
yield put(updateMarketData(data));
}
}
export function * initWebSocket() {
yield takeEvery(INITIALIZE_WS_CHANNEL, initializeWebSocketsChannel);
}
export default function* rootSaga() {
yield all ([
fork(initWebSocket)
]);
}
UPDATE
To complete the accepted answer by #azundo for someone looking for a complete example of websocket & redux-saga I'm adding following code:
function * initializeWebSocketsChannel() {
console.log("going to connect to WS")
const channel = yield call(createEventChannel);
while (true) {
const {data} = yield take(channel);
yield put(updateMarketData(data));
}
}
export function * startStopChannel() {
while (true) {
yield take(START_CHANNEL);
yield race({
task: call(initializeWebSocketsChannel),
cancel: take(STOP_CHANNEL),
});
//if cancel wins the race we can close socket
ws.close();
}
}
export default function* rootSaga() {
yield all ([
startStopChannel()
]);
}
The START_CHANNEL and STOP_CHANNEL actions can be called in componentDidMount and componentWillUnmount of react component life cycle, respectively.

The reason this isn't working is because your recursive call to createEventChannel is not being yielded to the saga middleware redux-saga has no way of knowing of the subsequent event channel creations. You'll want your recursive function to be defined within the event channel instead, see code below, so there is only one eventChannel that is always hooked into the store.
Also note the addition of emitting END on an expected socket close so that you don't leave the eventChannel open forever if you don't reconnect.
import {eventChannel, END} from 'redux-saga';
let ws; //define it here so it's available in return function
function createEventChannel() {
return eventChannel(emit => {
function createWs() {
//Subscribe to websocket
ws = new WebSocket('ws://localhost:9000/rates');
ws.onopen = () => {
console.log("Opening Websocket");
};
ws.onerror = error => {
console.log("ERROR: ", error);
};
ws.onmessage = e => {
return emit({data: JSON.parse(e.data)})
};
ws.onclose = e => {
if (e.code === 1005) {
console.log("WebSocket: closed");
// you probably want to end the channel in this case
emit(END);
} else {
console.log('Socket is closed Unexpectedly. Reconnect will be attempted in 4 second.', e.reason);
setTimeout(() => {
createWs();
}, 4000);
}
};
}
createWs();
return () => {
console.log("Closing Websocket");
ws.close();
};
});
}

Related

How to access a registered workbox queue in my React application

I'm using the create-react-app built in service worker for my PWA. I'm using the Queue class from workbox to add failed api calls to the queue. All the registration of the service worker is done in my service-worker.ts file.
My question: How can I access an already registered queue in my React component? I want to display a counter stating the amount of unsycned api calls. It is not clear to me how I can achieve this.
service-worker.ts
// add failed fetch requests to queue
const queue = new Queue('fetch-requests');
self.addEventListener('fetch', (event) => {
if (!self.navigator.onLine) {
const promiseChain = fetch(event.request.clone()).catch(() => {
if (event.request.method === 'POST' || event.request.method === 'PATCH') {
return queue.pushRequest({ request: event.request });
}
});
event.waitUntil(promiseChain);
}
});
App.tsx
const App: React.FC = () => {
// how can I access the queue from here and do some stuff with it?
return <Router />;
};
export default App;
Your queues are stored in indexedDB as "workbox-background-sync".
So, it's just a matter of querying the store. See example below
function getQueuesMeta() {
return new Promise(resolve => {
const connection = indexedDB.open("workbox-background-sync")
const res = []
connection.onsuccess = (event) => {
const db = event.target.result
try{
const tx = db.transaction(["requests"])
const reqIndex = tx.objectStore('requests').index('queueName')
reqIndex.openCursor().onsuccess = (e) => {
const cursor = e.target.result
let meta = {}
if (cursor) {
meta = {
// if you got meta, time to get it
timestamp: cursor.value.timestamp
}
if ('undefined' == typeof res[cursor.key]) {
res[cursor.key] = [meta]
} else {
res[cursor.key].push(meta)
}
cursor.continue()
} else {
resolve(res)
}
}
} catch (error){
console.log('no ‘request’ objectStore')
resolve(null)
}
}
})
}

eventChannel emitter action is not taken from redux-saga

I have create saga watcher to connecting websocket and listen received data. const payload = yield take(socketChannel); is waiting for take received message, but it not all received from
emit({type: 'WEBSOCKET_MESSAGE_RECEIVED', message});
Can someone help to find the issue?
function createWebSocketConnection() {
return new Promise((resolve) => {
websocket.onOpen(() => {
makeRequests(websocket);
resolve(websocket);
});
websocket.connect(true);
});
}
function createSocketChannel(socket) {
return eventChannel((emit) => {
socket.onMessage((message) => {
if (message.path) {
console.log('Emitting received data...');
return emit({type: 'WEBSOCKET_MESSAGE_RECEIVED', message});
}
});
socket.onClose(() => {
emit(END);
});
socket.onError(() => {
emit(END);
});
const unsubscribe = () => {
socket.onMessage = null;
};
return unsubscribe;
});
}
function* listenForSocketMessages() {
let socket;
let socketChannel;
try {
socket = yield call(createWebSocketConnection);
socketChannel = yield call(createSocketChannel, socket);
// tell the application that we have a connection
yield dispatch(ActionCreators.wsClientOpened());
while (true) {
// wait for a message from the channel
const payload = yield take(socketChannel);
console.log('new payload');
// a message has been received, dispatch an action with the message payload
yield dispatch(createAction(payload.path, payload));
}
}
catch (error) {
// yield dispatch(ActionCreators.wsClientError());
}
finally {
if (yield cancelled()) {
// close the channel
socketChannel.close();
// close the WebSocket connection
socket.close();
}
else {
// yield dispatch(ActionCreators.wsClientError());
}
}
}
const createAction = (type: string, payload?: any) => ({
type,
payload,
});
export default function* watchConnectWebsocket() {
// starts the task in the background
const socketTask = yield fork(listenForSocketMessages);
// when DISCONNECT action is dispatched, we cancel the socket task
yield take(WsActionTypes.WEBSOCKET_CLOSED);
yield cancel(socketTask);
yield dispatch(ActionCreators.wsClientClosed());
}

How to set timeout for yield call in React Redux-Saga

In my rest API backend I do heavy processing and usually, it takes 1.5 minutes to produce a result, in that time I'm getting this error in my frontend react application.
Error: timeout of 60000ms exceeded
So, peer connection is lost.
How do I set request timeout in redux-saga
i was used race for such things. May be it will useful for you.
const {posts, timeout} = yield race({
posts: call(fetchApi, '/posts'),
timeout: delay(60 * 1000)
});
if (timeout) throw new Error('timeout of 60000ms exceeded')
import { eventChannel, END } from 'redux-saga'
function countdown(secs) {
return eventChannel(emitter => {
const iv = setInterval(() => {
secs -= 1
if (secs > 0) {
emitter(secs)
} else {
// this causes the channel to close
emitter(END)
}
}, 1000);
// The subscriber must return an unsubscribe function
return () => {
clearInterval(iv)
}
}
)
}
Hope this helps.
export function* create(action) {
try {
const { payload } = action;
const response = yield call(api.addPost, payload);
if (response.status === 200) {
console.log('pass 200 check');
yield put(appActions.setResourceResponse(response.data));
console.log(response.data);
payload.push('/add-news');
}
} catch (error) {
console.log(error);
yield put(
a.setResponse({
message: error.response.data,
status: error.response.status,
}),
);
}
}

How to chain firebase observables using redux-saga?

I've recently made the move from ionic/angular2/Rxjs to React/React Native/Redux-sagas. I'm porting over my app from ionic to react native and have really enjoyed the process. One thing, however, I have really struggled with is using firebase in react native via redux sagas. I can do simple requests however I would like to chain three requests and get the return value as an JSON object. Currently I have this :
export const getTuteeUID = state => state.login.username;
function* getMemberProfiles(UID) {
try {
const memberObject = yield call(get, 'userProfile', UID);
return memberObject;
} catch (err) {
console.log(err);
}
}
function* getLatestConversation(grpID) {
try {
const opts = newOpts();
const refLatestConvo = firebase
.database()
.ref('conversations')
.child(`${grpID}`)
.orderByChild('createdAt')
.limitToLast(1);
yield call([refLatestConvo, refLatestConvo.on], 'value', opts.handler);
while (true) {
const { data } = yield take(opts);
if (data.val()) {
return data.val()[data._childKeys[0]];
}
return null;
}
} catch (err) {
console.log(err);
}
}
export function* getChatGroupObject(grpID, tuteeUID) {
try {
const groupObject = yield call(get, 'groups', grpID);
const memberProfiles = yield Object.keys(groupObject.members)
.filter(mem => mem !== tuteeUID)
.map(memUID => call(getMemberProfiles, memUID));
const latestConversation = yield call(getLatestConversation, grpID);
return { ...groupObject, memberProfiles, key: grpID, latestConversation };
} catch (err) {
console.log(err);
}
}
/**
*
* #return {Generator} []
*/
export function* fetchChatGroups() {
try {
const opts = newOpts();
const tuteeUID = yield select(getTuteeUID);
const refGrpIDs = firebase.database().ref('userProfile').child(`${tuteeUID}/groups`);
const snapGrpIDs = yield call([refGrpIDs, refGrpIDs.once], 'value', opts.handler);
if (snapGrpIDs.val()) {
const groupObject = yield Object.keys(snapGrpIDs.val()).map(grpID =>
call(getChatGroupObject, grpID, tuteeUID),
);
yield put(ChatAction.chatGroupsReceived(groupObject));
} else {
ChatAction.chatGroupsReceived([]);
}
} catch (error) {
console.log(error);
}
}
Now this works and returns the correct object, however if the latest conversation in the array changes the object won't update. How can I get this to continue updating? Another thing is if I were to put this in a while(true) loop, is there a way to unsubscribe from the observable? In rxjs this used to be super easy to do.

Redux saga, axios and progress event

Is there clean/short/right way to using together axios promise and uploading progress event?
Suppose I have next upload function:
function upload(payload, onProgress) {
const url = '/sources/upload';
const data = new FormData();
data.append('source', payload.file, payload.file.name);
const config = {
onUploadProgress: onProgress,
withCredentials: true
};
return axios.post(url, data, config);
}
This function returned the promise.
Also I have a saga:
function* uploadSaga(action) {
try {
const response = yield call(upload, payload, [?? anyProgressFunction ??]);
yield put({ type: UPLOADING_SUCCESS, payload: response });
} catch (err) {
yield put({ type: UPLOADING_FAIL, payload: err });
}
}
I want to receive progress events and put it by saga. Also I want to catch success (or failed) result of the axios request. Is it possible?
Thanks.
So I found the answer, thanks Mateusz Burzyński for the clarification.
We need use eventChannel, but a bit canningly.
Suppose we have api function for uploading file:
function upload(payload, onProgress) {
const url = '/sources/upload';
const data = new FormData();
data.append('source', payload.file, payload.file.name);
const config = {
onUploadProgress: onProgress,
withCredentials: true
};
return axios.post(url, data, config);
}
In saga we need to create eventChannel but put emit outside.
function createUploader(payload) {
let emit;
const chan = eventEmitter(emitter => {
emit = emitter;
return () => {}; // it's necessarily. event channel should
// return unsubscribe function. In our case
// it's empty function
});
const uploadPromise = upload(payload, (event) => {
if (event.loaded.total === 1) {
emit(END);
}
emit(event.loaded.total);
});
return [ uploadPromise, chan ];
}
function* watchOnProgress(chan) {
while (true) {
const data = yield take(chan);
yield put({ type: 'PROGRESS', payload: data });
}
}
function* uploadSource(action) {
const [ uploadPromise, chan ] = createUploader(action.payload);
yield fork(watchOnProgress, chan);
try {
const result = yield call(() => uploadPromise);
put({ type: 'SUCCESS', payload: result });
} catch (err) {
put({ type: 'ERROR', payload: err });
}
}
I personally found the accepted answer to be very convoluted, and I was having a hard time implementing it. Other google / SO searches all led to similar type answers. If it worked for you, great, but I found another way using an EventEmitter that I personally find much simpler.
Create an event emitter somewhere in your code:
// emitter.js
import { EventEmitter } from "eventemitter3";
export default new EventEmitter();
In your saga to make the api call, use this emitter to emit an event within the onUploadProgress callback:
// mysagas.js
import eventEmitter from '../wherever/emitter';
function upload(payload) {
// ...
const config = {
onUploadProgress: (progressEvent) = {
eventEmitter.emit(
"UPLOAD_PROGRESS",
Math.floor(100 * (progressEvent.loaded / progressEvent.total))
);
}
};
return axios.post(url, data, config);
}
Then in your component that needs this upload progress number, you can listen for this event on mount:
// ProgressComponent.jsx
import eventEmitter from '../wherever/emitter';
const ProgressComponent = () => {
const. [uploadProgress, setUploadProgress] = useState(0);
useEffect(() => {
eventEmitter.on(
"UPLOAD_PROGRESS",
percent => {
// latest percent available here, and will fire every time its updated
// do with it what you need, i.e. update local state, store state, etc
setUploadProgress(percent)
}
);
// stop listening on unmount
return function cleanup() {
eventEmitter.off("UPLOAD_PROGRESS")
}
}, [])
return <SomeLoadingBar value={percent} />
}
This worked for me as my application was already making use of a global eventEmitter for other reasons. I found this easier to implement, maybe someone else will too.

Resources