Last active
September 12, 2023 06:32
-
-
Save samoshkin/403b878319dd6b86a41f3cd92224ce43 to your computer and use it in GitHub Desktop.
Socket.IO and redux-saga integration. Connection management
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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