Improve reconnect handling

This commit is contained in:
Alan Friedman 2019-05-27 11:20:50 -04:00
parent f4eb1ae4b0
commit 409376daf0
9 changed files with 106 additions and 101 deletions

View File

@ -4,15 +4,7 @@ import {
process as processMessage, process as processMessage,
prepare as prepareMessage, prepare as prepareMessage,
} from 'utils/message' } from 'utils/message'
import { getIO } from 'utils/socket' import { getSocket } from 'utils/socket'
export const createRoom = id => async dispatch => fetch({
resourceName: 'handshake',
method: 'POST',
body: {
roomId: id,
},
}, dispatch, 'handshake')
export const receiveSocketMessage = payload => async (dispatch, getState) => { export const receiveSocketMessage = payload => async (dispatch, getState) => {
const state = getState() const state = getState()
@ -26,7 +18,7 @@ export const createUser = payload => async (dispatch) => {
} }
export const sendUserEnter = payload => async () => { export const sendUserEnter = payload => async () => {
getIO().emit('USER_ENTER', { getSocket().emit('USER_ENTER', {
publicKey: payload.publicKey, publicKey: payload.publicKey,
}) })
} }
@ -34,6 +26,11 @@ export const sendUserEnter = payload => async () => {
export const receiveUserExit = payload => async (dispatch, getState) => { export const receiveUserExit = payload => async (dispatch, getState) => {
const state = getState() const state = getState()
const exitingUser = state.room.members.find(m => !payload.map(p => JSON.stringify(p.publicKey)).includes(JSON.stringify(m.publicKey))) const exitingUser = state.room.members.find(m => !payload.map(p => JSON.stringify(p.publicKey)).includes(JSON.stringify(m.publicKey)))
if (!exitingUser) {
return;
}
const exitingUserId = exitingUser.id const exitingUserId = exitingUser.id
const exitingUsername = exitingUser.username const exitingUsername = exitingUser.username
@ -59,12 +56,12 @@ export const sendSocketMessage = payload => async (dispatch, getState) => {
const state = getState() const state = getState()
const msg = await prepareMessage(payload, state) const msg = await prepareMessage(payload, state)
dispatch({ type: `SEND_SOCKET_MESSAGE_${msg.original.type}`, payload: msg.original.payload }) dispatch({ type: `SEND_SOCKET_MESSAGE_${msg.original.type}`, payload: msg.original.payload })
getIO().emit('PAYLOAD', msg.toSend) getSocket().emit('PAYLOAD', msg.toSend)
} }
export const toggleLockRoom = () => async (dispatch, getState) => { export const toggleLockRoom = () => async (dispatch, getState) => {
const state = getState() const state = getState()
getIO().emit('TOGGLE_LOCK_ROOM', null, (res) => { getSocket().emit('TOGGLE_LOCK_ROOM', null, (res) => {
dispatch({ dispatch({
type: 'TOGGLE_LOCK_ROOM', type: 'TOGGLE_LOCK_ROOM',
payload: { payload: {
@ -82,6 +79,7 @@ export const receiveToggleLockRoom = payload => async (dispatch, getState) => {
const lockedByUser = state.room.members.find(m => isEqual(m.publicKey, payload.publicKey)) const lockedByUser = state.room.members.find(m => isEqual(m.publicKey, payload.publicKey))
const lockedByUsername = lockedByUser.username const lockedByUsername = lockedByUser.username
const lockedByUserId = lockedByUser.id const lockedByUserId = lockedByUser.id
console.log('locked by', lockedByUserId);
dispatch({ dispatch({
type: 'RECEIVE_TOGGLE_LOCK_ROOM', type: 'RECEIVE_TOGGLE_LOCK_ROOM',
@ -96,3 +94,12 @@ export const receiveToggleLockRoom = payload => async (dispatch, getState) => {
export const clearActivities = () => async (dispatch) => { export const clearActivities = () => async (dispatch) => {
dispatch({ type: 'CLEAR_ACTIVITIES' }) dispatch({ type: 'CLEAR_ACTIVITIES' })
} }
export const onConnected = payload => async (dispatch) => {
dispatch({ type: 'CONNECTED', payload })
}
export const sendUserDisconnect = () => async () => {
getSocket().emit('USER_DISCONNECT')
}

View File

@ -43,56 +43,32 @@ export default class Home extends Component {
async componentWillMount() { async componentWillMount() {
const roomId = encodeURI(this.props.match.params.roomId) const roomId = encodeURI(this.props.match.params.roomId)
const res = await this.props.createRoom(roomId)
if (res.json.isLocked) {
this.props.openModal('Room Locked')
return
}
if (res.json.size === 1) {
this.props.openModal('Welcome')
}
const user = await this.createUser() const user = await this.createUser()
const io = connect(roomId) const socket = connect(roomId)
const disconnectEvents = [ const disconnectEvents = [
'reconnect_failed',
'connect_timeout',
'connect_error',
'disconnect', 'disconnect',
'reconnect',
'reconnect_error',
'reconnecting',
'reconnect_attempt',
] ]
disconnectEvents.forEach((evt) => { disconnectEvents.forEach((evt) => {
io.on(evt, () => { socket.on(evt, () => {
this.props.toggleSocketConnected(false) this.props.toggleSocketConnected(false)
}) })
}) })
const connectEvents = [ const connectEvents = [
'connect', 'connect',
'reconnect',
] ]
connectEvents.forEach((evt) => { connectEvents.forEach((evt) => {
io.on(evt, () => { socket.on(evt, () => {
if (evt === 'connect') { this.initApp(user)
if (!this.hasConnected) {
this.initApp(user)
this.hasConnected = true
}
}
this.props.toggleSocketConnected(true) this.props.toggleSocketConnected(true)
}) })
}) })
io.on('USER_ENTER', (payload) => { socket.on('USER_ENTER', (payload) => {
this.props.receiveUserEnter(payload) this.props.receiveUserEnter(payload)
this.props.sendSocketMessage({ this.props.sendSocketMessage({
type: 'ADD_USER', type: 'ADD_USER',
@ -105,17 +81,29 @@ export default class Home extends Component {
}) })
}) })
io.on('USER_EXIT', (payload) => { socket.on('USER_EXIT', (payload) => {
this.props.receiveUserExit(payload) this.props.receiveUserExit(payload)
}) })
io.on('PAYLOAD', (payload) => { socket.on('PAYLOAD', (payload) => {
this.props.receiveSocketMessage(payload) this.props.receiveSocketMessage(payload)
}) })
io.on('TOGGLE_LOCK_ROOM', (payload) => { socket.on('TOGGLE_LOCK_ROOM', (payload) => {
this.props.receiveToggleLockRoom(payload) this.props.receiveToggleLockRoom(payload)
}) })
socket.on('CONNECTED', (payload) => {
this.props.onConnected(payload);
});
socket.on('ROOM_LOCKED', (payload) => {
this.props.openModal('Room Locked')
});
window.addEventListener('beforeunload', (evt) => {
this.props.sendUserDisconnect();
});
} }
componentDidMount() { componentDidMount() {
@ -135,7 +123,7 @@ export default class Home extends Component {
Tinycon.setBubble(nextProps.faviconCount) Tinycon.setBubble(nextProps.faviconCount)
if (nextProps.faviconCount !== 0 && this.props.soundIsEnabled) { if (nextProps.faviconCount !== 0 && nextProps.faviconCount !== this.props.faviconCount && this.props.soundIsEnabled) {
this.beep.play() this.beep.play()
} }
} }
@ -423,7 +411,6 @@ Home.defaultProps = {
} }
Home.propTypes = { Home.propTypes = {
createRoom: PropTypes.func.isRequired,
receiveSocketMessage: PropTypes.func.isRequired, receiveSocketMessage: PropTypes.func.isRequired,
sendSocketMessage: PropTypes.func.isRequired, sendSocketMessage: PropTypes.func.isRequired,
createUser: PropTypes.func.isRequired, createUser: PropTypes.func.isRequired,
@ -445,6 +432,7 @@ Home.propTypes = {
scrolledToBottom: PropTypes.bool.isRequired, scrolledToBottom: PropTypes.bool.isRequired,
iAmOwner: PropTypes.bool.isRequired, iAmOwner: PropTypes.bool.isRequired,
sendUserEnter: PropTypes.func.isRequired, sendUserEnter: PropTypes.func.isRequired,
sendUserDisconnect: PropTypes.func.isRequired,
userId: PropTypes.string.isRequired, userId: PropTypes.string.isRequired,
joining: PropTypes.bool.isRequired, joining: PropTypes.bool.isRequired,
toggleWindowFocus: PropTypes.func.isRequired, toggleWindowFocus: PropTypes.func.isRequired,
@ -453,4 +441,5 @@ Home.propTypes = {
toggleSoundEnabled: PropTypes.func.isRequired, toggleSoundEnabled: PropTypes.func.isRequired,
toggleSocketConnected: PropTypes.func.isRequired, toggleSocketConnected: PropTypes.func.isRequired,
socketConnected: PropTypes.bool.isRequired, socketConnected: PropTypes.bool.isRequired,
onConnected: PropTypes.func.isRequired
} }

View File

@ -1,7 +1,6 @@
import { connect } from 'react-redux' import { connect } from 'react-redux'
import Home from 'components/Home' import Home from 'components/Home'
import { import {
createRoom,
receiveSocketMessage, receiveSocketMessage,
sendSocketMessage, sendSocketMessage,
createUser, createUser,
@ -16,6 +15,8 @@ import {
toggleWindowFocus, toggleWindowFocus,
toggleSoundEnabled, toggleSoundEnabled,
toggleSocketConnected, toggleSocketConnected,
onConnected,
sendUserDisconnect
} from 'actions' } from 'actions'
const mapStateToProps = (state) => { const mapStateToProps = (state) => {
@ -41,7 +42,6 @@ const mapStateToProps = (state) => {
} }
const mapDispatchToProps = { const mapDispatchToProps = {
createRoom,
receiveSocketMessage, receiveSocketMessage,
sendSocketMessage, sendSocketMessage,
receiveUserExit, receiveUserExit,
@ -56,6 +56,8 @@ const mapDispatchToProps = {
toggleWindowFocus, toggleWindowFocus,
toggleSoundEnabled, toggleSoundEnabled,
toggleSocketConnected, toggleSocketConnected,
onConnected,
sendUserDisconnect
} }
export default connect( export default connect(

View File

@ -90,13 +90,6 @@ const activities = (state = initialState, action) => {
return state return state
} }
// Duplicate "user entered" can happen when >2 users join
// in quick succession
const alreadyEntered = state.items.find(act => act.type === 'USER_ENTER' && act.userId === newUserId)
if (alreadyEntered) {
return state
}
if (action.payload.state.room.joining) { if (action.payload.state.room.joining) {
return state return state
} }

View File

@ -9,8 +9,6 @@ const initialState = {
const app = (state = initialState, action) => { const app = (state = initialState, action) => {
switch (action.type) { switch (action.type) {
case 'FETCH_CREATE_HANDSHAKE_SUCCESS':
return state;
case 'OPEN_MODAL': case 'OPEN_MODAL':
return { return {
...state, ...state,

View File

@ -15,15 +15,14 @@ const initialState = {
const room = (state = initialState, action) => { const room = (state = initialState, action) => {
switch (action.type) { switch (action.type) {
case 'FETCH_CREATE_HANDSHAKE_SUCCESS': case 'CONNECTED':
const isLocked = action.payload.json.isLocked const size = action.payload.users ? action.payload.users.length : 1;
// Handle "room is locked" message for new members here
return { return {
...state, ...state,
id: action.payload.json.id, id: action.payload.id,
isLocked, isLocked: Boolean(action.payload.isLocked),
size: action.payload.json.size, size,
joining: !(action.payload.json.size === 1), joining: false
} }
case 'USER_EXIT': case 'USER_EXIT':
const memberPubKeys = action.payload.members.map(m => JSON.stringify(m.publicKey)) const memberPubKeys = action.payload.members.map(m => JSON.stringify(m.publicKey))
@ -41,7 +40,7 @@ const room = (state = initialState, action) => {
} }
case 'HANDLE_SOCKET_MESSAGE_ADD_USER': case 'HANDLE_SOCKET_MESSAGE_ADD_USER':
const membersWithId = state.members.filter(m => m.id) const membersWithId = state.members.filter(m => m.id)
const joining = state.joining ? membersWithId.length + 1 < state.size : false const joining = false
return { return {
...state, ...state,
@ -79,24 +78,29 @@ const room = (state = initialState, action) => {
will receive a USER_ENTER event that doesn't contain itself. In that case we will receive a USER_ENTER event that doesn't contain itself. In that case we
want to prepend "me" to the members payload want to prepend "me" to the members payload
*/ */
const diff = _.differenceBy(state.members, action.payload, m => m.publicKey.n) const members = _.uniqBy(action.payload, member => member.publicKey.n);
const members = diff.length ? state.members.concat(action.payload) : action.payload
return { return {
...state, ...state,
members: members.map((user) => { members: members.reduce((acc, user) => {
const exists = state.members.find(m => _.isEqual(m.publicKey, user.publicKey)) const exists = state.members.find(m => m.publicKey.n === user.publicKey.n)
if (exists) { if (exists) {
return { return [
...user, ...acc,
...exists, {
...user,
...exists,
}
]
}
return [
...acc,
{
publicKey: user.publicKey,
isOwner: user.isOwner,
id: user.id,
} }
} ]
return { }, []),
publicKey: user.publicKey,
isOwner: user.isOwner,
id: user.id,
}
}),
} }
case 'TOGGLE_LOCK_ROOM': case 'TOGGLE_LOCK_ROOM':
return { return {

View File

@ -13,4 +13,4 @@ export const connect = (roomId) => {
return socket return socket
} }
export const getIO = () => socket export const getSocket = () => socket

View File

@ -42,25 +42,6 @@ if ((siteURL || env === 'development') && !isReviewApp) {
})); }));
} }
router.post('/handshake', koaBody, async (ctx) => {
const { body } = ctx.request;
const { roomId } = body;
const roomIdHash = getRoomIdHash(roomId)
let roomExists = await redis.hgetAsync('rooms', roomIdHash)
if (roomExists) {
roomExists = JSON.parse(roomExists)
}
ctx.body = {
id: roomId,
ready: true,
isLocked: Boolean(roomExists && roomExists.isLocked),
size: ((roomExists && roomExists.users.length) || 0) + 1,
};
});
router.post('/abuse/:roomId', koaBody, async (ctx) => { router.post('/abuse/:roomId', koaBody, async (ctx) => {
let { roomId } = ctx.params; let { roomId } = ctx.params;
@ -120,7 +101,10 @@ if (clientDistDirectory) {
const protocol = (process.env.PROTOCOL || 'http') === 'http' ? http : https; const protocol = (process.env.PROTOCOL || 'http') === 'http' ? http : https;
const server = protocol.createServer(app.callback()); const server = protocol.createServer(app.callback());
const io = Io(server); const io = Io(server, {
pingInterval: 5000,
pingTimeout: 3000
});
io.adapter(socketRedis(process.env.REDIS_URL)); io.adapter(socketRedis(process.env.REDIS_URL));
const roomHashSecret = process.env.ROOM_HASH_SECRET; const roomHashSecret = process.env.ROOM_HASH_SECRET;
@ -151,6 +135,7 @@ io.on('connection', async (socket) => {
room = JSON.parse(room || '{}') room = JSON.parse(room || '{}')
new Socket({ new Socket({
roomIdOriginal: roomId,
roomId: roomIdHash, roomId: roomIdHash,
socket, socket,
room, room,

View File

@ -4,10 +4,14 @@ import { getIO, getRedis } from './index'
export default class Socket { export default class Socket {
constructor(opts) { constructor(opts) {
const { roomId, socket, room } = opts const { roomId, socket, room, roomIdOriginal } = opts
this._roomId = roomId this._roomId = roomId
this.socket = socket;
this.roomIdOriginal = roomIdOriginal;
this.room = room;
if (room.isLocked) { if (room.isLocked) {
this.sendRoomLocked();
return return
} }
@ -17,9 +21,28 @@ export default class Socket {
async init(opts) { async init(opts) {
const { roomId, socket, room } = opts const { roomId, socket, room } = opts
await this.joinRoom(roomId, socket.id) await this.joinRoom(roomId, socket.id)
this.sendRoomInfo();
this.handleSocket(socket) this.handleSocket(socket)
} }
sendRoomInfo() {
let room;
if (_.isEmpty(this.room)) {
room = {
id: this.roomIdOriginal,
users: [],
isLocked: false,
}
} else {
room = this.room;
}
this.socket.emit('CONNECTED', room);
}
sendRoomLocked() {
this.socket.emit('ROOM_LOCKED');
}
async saveRoom(room) { async saveRoom(room) {
const json = { const json = {
...room, ...room,
@ -109,6 +132,8 @@ export default class Socket {
}); });
socket.on('disconnect', () => this.handleDisconnect(socket)); socket.on('disconnect', () => this.handleDisconnect(socket));
socket.on('USER_DISCONNECT', () => this.handleDisconnect(socket));
} }
async handleDisconnect(socket) { async handleDisconnect(socket) {
@ -129,6 +154,8 @@ export default class Socket {
if (newRoom.users && newRoom.users.length === 0) { if (newRoom.users && newRoom.users.length === 0) {
await this.destroyRoom() await this.destroyRoom()
} }
socket.disconnect(true);
} }
} }