Skip to content

Instantly share code, notes, and snippets.

@samoshkin
Last active September 12, 2023 06:32
Show Gist options
  • Save samoshkin/403b878319dd6b86a41f3cd92224ce43 to your computer and use it in GitHub Desktop.
Save samoshkin/403b878319dd6b86a41f3cd92224ce43 to your computer and use it in GitHub Desktop.
Socket.IO and redux-saga integration. Connection management
import { io } from 'socket.io-client';
import * as SentrySDK from '@sentry/react';
import {
call,
fork,
takeEvery,
put,
race,
take,
} from 'redux-saga/effects';
import {
fromEvent,
fromEvents,
} from '../../../classes/utils/saga';
import routes from '../../../configs/constants/routes';
import { AppEventType } from '../../../classes/enums';
export default function* run() {
const socket = io.connect('/connect', {
transports: ['websocket'],
path: routes.streamServer,
// whether to add the timestamp query param to each request (for cache busting)
timestampRequests: true,
// automatic reconnection policy
reconnection: true,
reconnectionAttempts: 6,
reconnectionDelay: 500,
reconnectionDelayMax: 15000,
// timeout for each connection attempt
timeout: 10000,
});
yield fork(manageSocketConnection, socket);
yield fork(listenSocketMessages, socket);
}
function* manageSocketConnection(socket) {
const socketChannel = fromEvents(
socket,
[
'connect',
'disconnect',
// the low-level connection cannot be established
// the connection is denied by the server in a middleware function
'connect_error',
],
);
let isReconnecting = false;
const isConnected = () => socket.connected;
yield fork(reconnectImmediatelyWhenBackOnline);
yield fork(detectReconnects);
while (true) {
// wait either for connect or connect_error
const [, connectErrorEvent] = yield race([
take(socketChannel, 'connect'),
take(socketChannel, 'connect_error'),
]);
// the connection is denied by the server in a middleware function
// socket.io will not attempt any reconnects
// report error to Sentry
if (connectErrorEvent?.payload?.data?.errorCode) {
SentrySDK.captureException(connectErrorEvent.payload, {
extra: {
silent: true,
},
});
}
// ok, it's connected now
if (isConnected()) {
yield put({ type: AppEventType.Client.Connected });
// wait for disconnect
const { payload: [reason, details] } = yield take(socketChannel, 'disconnect');
yield put({
type: AppEventType.Client.Disconnected,
payload: {
reason,
details,
// reasons: https://socket.io/docs/v4/client-socket-instance/#disconnect
// io server disconnect: server has forcefully disconnected the socket with socket.disconnect()
// io client disconnect, socket was manually disconnected using socket.disconnect() on the client
// in both cases, socket.io will not make any reconnection attempts
wasExplicitlyDisconnected: reason === 'io server disconnect'
|| reason === 'io client disconnect',
},
});
}
// at this point, the loop wraps up and starts from the beginning
// thus waiting for next connect
}
function* detectReconnects() {
const managerChannel = fromEvents(
socket.io,
[
'reconnect_attempt', // reconnect attempt
'reconnect', // succesful reconnect attempt
'reconnect_error', // failed reconnect attempt
'open', // underlying web socket network connection is restored
'reconnect_failed', // all reconnect attempts failed
],
);
while (true) {
let reconnectErr = null;
// detects first reconnect attempt
yield take(managerChannel, 'reconnect_attempt');
yield put({ type: AppEventType.Client.Reconnecting });
isReconnecting = true;
// wait either for successful reconnect or when all reconnect attempts fail
const [hasReconnectedFailed] = yield race([
take(managerChannel, 'reconnect_failed'),
take(managerChannel, 'reconnect'),
take(AppEventType.Client.Connected),
call(function* () {
// keep track of reconnect error
while (true) {
({ payload: reconnectErr } = yield take(managerChannel, 'reconnect_error'));
}
}),
]);
isReconnecting = false;
// when reconnect completely failed, report reconnect error to Sentry
if (hasReconnectedFailed) {
yield put({
type: AppEventType.Client.ReconnectFailed,
payload: reconnectErr,
});
SentrySDK.captureException(reconnectErr, {
extra: {
silent: true,
},
});
} else {
yield put({ type: AppEventType.Client.Reconnected });
}
}
}
function* reconnectImmediatelyWhenBackOnline() {
yield takeEvery(
fromEvent(window, 'online'),
function* () {
// if we are already reconnecting right now, cancel reconnect attempts
if (isReconnecting) {
yield call(() => { socket.disconnect(); });
}
// and forcefully connect once again
yield call(() => { socket.connect(); });
},
);
}
}
function* listenSocketMessages(socket) {
yield takeEvery(
fromEvent(socket, 'message'),
function* ({ payload }) {
yield put(payload);
},
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment